Merge branch 'release/3.3.0'

This commit is contained in:
Sojan 2023-11-17 20:37:25 -08:00
commit bda2d1d9a4
409 changed files with 6262 additions and 2530 deletions

View File

@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
# Mail Incoming # Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled # This is the domain set for the reply emails when conversation continuity is enabled
MAILER_INBOUND_EMAIL_DOMAIN= MAILER_INBOUND_EMAIL_DOMAIN=
# Set this to appropriate ingress channel with regards to incoming emails # Set this to the appropriate ingress channel with regards to incoming emails
# Possible values are : # Possible values are :
# relay for Exim, Postfix, Qmail # relay for Exim, Postfix, Qmail
# mailgun for Mailgun # mailgun for Mailgun
@ -91,10 +91,20 @@ MAILER_INBOUND_EMAIL_DOMAIN=
RAILS_INBOUND_EMAIL_SERVICE= RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service # Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html # Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
# Set this to a password of your choice and use it in the Inbound webhook
RAILS_INBOUND_EMAIL_PASSWORD= RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY= MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY= MANDRILL_INGRESS_API_KEY=
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
# Inbound webhook URL format:
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
# Note: Replace the values inside the brackets; do not include the brackets themselves.
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
# For Postmark
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
# Storage # Storage
ACTIVE_STORAGE_SERVICE=local ACTIVE_STORAGE_SERVICE=local
@ -176,6 +186,9 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# MICROSOFT CLARITY # MICROSOFT CLARITY
# MS_CLARITY_TOKEN=xxxxxxxxx # MS_CLARITY_TOKEN=xxxxxxxxx
# GOOGLE_TAG_MANAGER
# GOOGLE_TAG = GTM-XXXXXXX
## Scout ## Scout
## https://scoutapm.com/docs/ruby/configuration ## https://scoutapm.com/docs/ruby/configuration
# SCOUT_KEY=YOURKEY # SCOUT_KEY=YOURKEY
@ -240,3 +253,9 @@ AZURE_APP_SECRET=
# Sentiment analysis model file path # Sentiment analysis model file path
SENTIMENT_FILE_PATH= SENTIMENT_FILE_PATH=
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false

View File

@ -4,6 +4,7 @@ module.exports = {
'prettier', 'prettier',
'plugin:vue/recommended', 'plugin:vue/recommended',
'plugin:storybook/recommended', 'plugin:storybook/recommended',
'plugin:cypress/recommended',
], ],
parserOptions: { parserOptions: {
parser: '@babel/eslint-parser', parser: '@babel/eslint-parser',

12
Gemfile
View File

@ -107,16 +107,16 @@ gem 'google-cloud-translate-v3'
gem 'ddtrace', require: false gem 'ddtrace', require: false
gem 'elastic-apm', require: false gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', require: false gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'scout_apm', require: false gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.12.0', require: false gem 'sentry-rails', '>= 5.13.0', require: false
gem 'sentry-ruby', require: false gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.12.0', require: false gem 'sentry-sidekiq', '>= 5.13.0', require: false
##-- background job processing --## ##-- background job processing --##
gem 'sidekiq', '>= 7.1.3' gem 'sidekiq', '>= 7.1.3'
# We want cron jobs # We want cron jobs
gem 'sidekiq-cron', '>= 1.10.1' gem 'sidekiq-cron', '>= 1.11.0'
##-- Push notification service --## ##-- Push notification service --##
gem 'fcm' gem 'fcm'
@ -159,7 +159,7 @@ gem 'lograge', '~> 0.14.0', require: false
# worked with microsoft refresh token # worked with microsoft refresh token
gem 'omniauth-oauth2' gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.0' gem 'audited', '~> 5.4', '>= 5.4.1'
# need for google auth # need for google auth
gem 'omniauth' gem 'omniauth'
@ -200,6 +200,8 @@ group :development do
# profiling # profiling
gem 'rack-mini-profiler', '>= 3.1.1', require: false gem 'rack-mini-profiler', '>= 3.1.1', require: false
gem 'stackprof' gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request'
end end
group :test do group :test do

View File

@ -126,9 +126,9 @@ GEM
rake (>= 10.4, < 14.0) rake (>= 10.4, < 14.0)
ast (2.4.2) ast (2.4.2)
attr_extras (7.1.0) attr_extras (7.1.0)
audited (5.4.0) audited (5.4.1)
activerecord (>= 5.0, < 7.2) activerecord (>= 5.0, < 7.7)
request_store (~> 1.2) activesupport (>= 5.0, < 7.7)
aws-eventstream (1.2.0) aws-eventstream (1.2.0)
aws-partitions (1.760.0) aws-partitions (1.760.0)
aws-sdk-core (3.171.1) aws-sdk-core (3.171.1)
@ -148,6 +148,7 @@ GEM
barnes (0.0.9) barnes (0.0.9)
multi_json (~> 1) multi_json (~> 1)
statsd-ruby (~> 1.1) statsd-ruby (~> 1.1)
base64 (0.1.1)
bcrypt (3.1.19) bcrypt (3.1.19)
bindex (0.8.1) bindex (0.8.1)
blingfire (0.1.8) blingfire (0.1.8)
@ -174,7 +175,7 @@ GEM
rexml rexml
crass (1.0.6) crass (1.0.6)
csv-safe (3.2.1) csv-safe (3.2.1)
cypress-on-rails (1.13.1) cypress-on-rails (1.16.0)
rack rack
database_cleaner (2.0.2) database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (>= 2, < 3)
@ -261,7 +262,7 @@ GEM
rake rake
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
foreman (0.87.2) foreman (0.87.2)
fugit (1.8.1) fugit (1.9.0)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
gapic-common (0.18.0) gapic-common (0.18.0)
@ -450,13 +451,16 @@ GEM
marcel (1.0.2) marcel (1.0.2)
maxminddb (0.1.22) maxminddb (0.1.22)
memoist (0.16.2) memoist (0.16.2)
meta_request (0.7.4)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 7.1)
method_source (1.0.0) method_source (1.0.0)
mime-types (3.4.1) mime-types (3.4.1)
mime-types-data (~> 3.2015) mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1) mime-types-data (3.2023.0218.1)
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
mini_portile2 (2.8.4) mini_portile2 (2.8.5)
minitest (5.20.0) minitest (5.20.0)
mock_redis (0.36.0) mock_redis (0.36.0)
ruby2_keywords ruby2_keywords
@ -478,10 +482,11 @@ GEM
net-smtp (0.3.3) net-smtp (0.3.3)
net-protocol net-protocol
netrc (0.11.0) netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.1) newrelic-sidekiq-metrics (1.6.2)
newrelic_rpm (~> 8) newrelic_rpm (>= 8.0.0)
sidekiq sidekiq
newrelic_rpm (8.16.0) newrelic_rpm (9.6.0)
base64
nio4r (2.5.9) nio4r (2.5.9)
nokogiri (1.15.4) nokogiri (1.15.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
@ -553,10 +558,12 @@ GEM
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.7.1) racc (1.7.3)
rack (2.2.8) rack (2.2.8)
rack-attack (6.7.0) rack-attack (6.7.0)
rack (>= 1.0, < 4) rack (>= 1.0, < 4)
rack-contrib (2.4.0)
rack (< 4)
rack-cors (2.0.1) rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (3.1.1) rack-mini-profiler (3.1.1)
@ -597,13 +604,13 @@ GEM
thor (~> 1.0) thor (~> 1.0)
zeitwerk (~> 2.5) zeitwerk (~> 2.5)
rainbow (3.1.1) rainbow (3.1.1)
rake (13.0.6) rake (13.1.0)
rb-fsevent (0.11.2) rb-fsevent (0.11.2)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redis (5.0.6) redis (5.0.6)
redis-client (>= 0.9.0) redis-client (>= 0.9.0)
redis-client (0.17.0) redis-client (0.18.0)
connection_pool connection_pool
redis-namespace (1.10.0) redis-namespace (1.10.0)
redis (>= 4) redis (>= 4)
@ -702,23 +709,23 @@ GEM
activesupport (>= 4) activesupport (>= 4)
selectize-rails (0.12.6) selectize-rails (0.12.6)
semantic_range (3.0.0) semantic_range (3.0.0)
sentry-rails (5.12.0) sentry-rails (5.13.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.12.0) sentry-ruby (~> 5.13.0)
sentry-ruby (5.12.0) sentry-ruby (5.13.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.12.0) sentry-sidekiq (5.13.0)
sentry-ruby (~> 5.12.0) sentry-ruby (~> 5.13.0)
sidekiq (>= 3.0) sidekiq (>= 3.0)
sexp_processor (4.17.0) sexp_processor (4.17.0)
shoulda-matchers (5.3.0) shoulda-matchers (5.3.0)
activesupport (>= 5.2.0) activesupport (>= 5.2.0)
sidekiq (7.1.6) sidekiq (7.2.0)
concurrent-ruby (< 2) concurrent-ruby (< 2)
connection_pool (>= 2.3.0) connection_pool (>= 2.3.0)
rack (>= 2.2.4) rack (>= 2.2.4)
redis-client (>= 0.14.0) redis-client (>= 0.14.0)
sidekiq-cron (1.10.1) sidekiq-cron (1.11.0)
fugit (~> 1.8) fugit (~> 1.8)
globalid (>= 1.0.1) globalid (>= 1.0.1)
sidekiq (>= 6) sidekiq (>= 6)
@ -758,7 +765,7 @@ GEM
stripe (8.5.0) stripe (8.5.0)
telephone_number (1.4.20) telephone_number (1.4.20)
test-prof (1.2.1) test-prof (1.2.1)
thor (1.2.2) thor (1.3.0)
tilt (2.2.0) tilt (2.2.0)
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
@ -838,7 +845,7 @@ DEPENDENCIES
administrate-field-belongs_to_search administrate-field-belongs_to_search
annotate annotate
attr_extras attr_extras
audited (~> 5.4, >= 5.4.0) audited (~> 5.4, >= 5.4.1)
aws-sdk-s3 aws-sdk-s3
azure-storage-blob! azure-storage-blob!
barnes barnes
@ -893,9 +900,10 @@ DEPENDENCIES
listen listen
lograge (~> 0.14.0) lograge (~> 0.14.0)
maxminddb maxminddb
meta_request
mock_redis mock_redis
neighbor neighbor
newrelic-sidekiq-metrics newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm newrelic_rpm
omniauth omniauth
omniauth-google-oauth2 omniauth-google-oauth2
@ -927,12 +935,12 @@ DEPENDENCIES
scout_apm scout_apm
scss_lint scss_lint
seed_dump seed_dump
sentry-rails (>= 5.12.0) sentry-rails (>= 5.13.0)
sentry-ruby sentry-ruby
sentry-sidekiq (>= 5.12.0) sentry-sidekiq (>= 5.13.0)
shoulda-matchers shoulda-matchers
sidekiq (>= 7.1.3) sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.10.1) sidekiq-cron (>= 1.11.0)
simplecov (= 0.17.1) simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0) slack-ruby-client (~> 2.2.0)
spring spring

View File

@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050 backend: RAILS_ENV=test bin/rails s -p 5050
frontend: bin/webpack-dev-server frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml

View File

@ -1,10 +1,12 @@
<p align="center"> <img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" /> <img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p> ___
</p>
<p align="center"> # Chatwoot
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p>
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku"> <a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/> <img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a> </a>
@ -13,9 +15,7 @@
</a> </a>
</p> </p>
___ <p>
<p align="center">
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a> <a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge"> <img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a> <a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
@ -29,9 +29,8 @@ ___
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a> <a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p> </p>
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/> <img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile. Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.

View File

@ -76,6 +76,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present? message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
end end
def message_reply_attributes
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
end
def build_message def build_message
return if @outgoing_echo && already_sent_from_chatwoot? return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files? return if message_content.blank? && all_unsupported_files?
@ -118,7 +122,10 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: message_type, message_type: message_type,
source_id: message_identifier, source_id: message_identifier,
content: message_content, content: message_content,
sender: @outgoing_echo ? nil : contact sender: @outgoing_echo ? nil : contact,
content_attributes: {
in_reply_to_external_id: message_reply_attributes
}
} }
end end

View File

@ -9,11 +9,11 @@ class Messages::MessageBuilder
@user = user @user = user
@message_type = params[:message_type] || 'outgoing' @message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments] @attachments = params[:attachments]
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id) @automation_rule = content_attributes&.dig(:automation_rule_id)
return unless params.instance_of?(ActionController::Parameters) return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to) @in_reply_to = content_attributes&.dig(:in_reply_to)
@items = params.to_unsafe_h&.dig(:content_attributes, :items) @items = content_attributes&.dig(:items)
end end
def perform def perform
@ -26,6 +26,38 @@ class Messages::MessageBuilder
private private
# Extracts content attributes from the given params.
# - Converts ActionController::Parameters to a regular hash if needed.
# - Attempts to parse a JSON string if content is a string.
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
def content_attributes
params = convert_to_hash(@params)
content_attributes = params.fetch(:content_attributes, {})
return parse_json(content_attributes) if content_attributes.is_a?(String)
return content_attributes if content_attributes.is_a?(Hash)
{}
end
# Converts the given object to a hash.
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
# Otherwise, returns the object as-is.
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
# Attempts to parse a string as JSON.
# If successful, returns the parsed hash with symbolized names.
# If unsuccessful, returns nil.
def parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
def process_attachments def process_attachments
return if @attachments.blank? return if @attachments.blank?

View File

@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
@articles_count = @all_articles.count @articles_count = @all_articles.count
@articles = if list_params[:category_slug].present? @articles = if list_params[:category_slug].present?
@all_articles.order_by_position.page(@current_page).per(50) @all_articles.order_by_position.page(@current_page)
else else
@all_articles.order_by_updated_at.page(@current_page) @all_articles.order_by_updated_at.page(@current_page)
end end

View File

@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def category_params def category_params
params.require(:category).permit( params.require(:category).permit(
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id :name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
) )
end end

View File

@ -47,6 +47,11 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok head :ok
end end
def logo
@portal.logo.purge if @portal.logo.attached?
head :ok
end
def process_attached_logo def process_attached_logo
blob_id = params[:blob_id] blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_by(id: blob_id) blob = ActiveStorage::Blob.find_by(id: blob_id)

View File

@ -79,6 +79,9 @@ class Api::V1::Widget::BaseController < ApplicationController
sender: @contact, sender: @contact,
content: permitted_params[:message][:content], content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
content_attributes: {
in_reply_to: permitted_params[:message][:reply_to]
},
echo_id: permitted_params[:message][:echo_id], echo_id: permitted_params[:message][:echo_id],
message_type: :incoming message_type: :incoming
} }

View File

@ -28,7 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
conversation.contact_last_seen_at = DateTime.now.utc conversation.contact_last_seen_at = DateTime.now.utc
conversation.save! conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(conversation) ::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
head :ok head :ok
end end

View File

@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def permitted_params def permitted_params
# timestamp parameter is used in create conversation method # timestamp parameter is used in create conversation method
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
end end
def set_message def set_message

View File

@ -23,6 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
def update_last_seen def update_last_seen
@conversation.contact_last_seen_at = DateTime.now.utc @conversation.contact_last_seen_at = DateTime.now.utc
@conversation.save! @conversation.save!
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
head :ok head :ok
end end

View File

@ -39,10 +39,6 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
) )
end end
def portal
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
end
def list_params def list_params
params.permit(:query, :locale, :sort, :status) params.permit(:query, :locale, :sort, :status)
end end

View File

@ -14,10 +14,14 @@ class Public::Api::V1::Portals::BaseController < PublicController
@theme = if %w[dark light].include?(params[:theme]) @theme = if %w[dark light].include?(params[:theme])
params[:theme] params[:theme]
else else
'' 'system'
end end
end end
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
end
def set_locale(&) def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present? switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present? switch_locale_with_article(&) if params[:article_slug].present?
@ -40,6 +44,8 @@ class Public::Api::V1::Portals::BaseController < PublicController
def switch_locale_with_article(&) def switch_locale_with_article(&)
article = Article.find_by(slug: params[:article_slug]) article = Article.find_by(slug: params[:article_slug])
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
render_404 && return if article.blank?
@locale = if article.category.present? @locale = if article.category.present?
article.category.locale article.category.locale
@ -53,4 +59,9 @@ class Public::Api::V1::Portals::BaseController < PublicController
def allow_iframe_requests def allow_iframe_requests
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
end end
def render_404
portal
render 'public/api/v1/portals/error/404', status: :not_found
end
end end

View File

@ -13,10 +13,9 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
private private
def set_category def set_category
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug]) @category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
end
def portal Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
@portal ||= Portal.find_by!(slug: params[:slug], archived: false) render_404 && return if @category.blank?
end end
end end

View File

@ -1,11 +1,48 @@
module PortalHelper module PortalHelper
def generate_portal_bg_color(portal_color, theme) def generate_portal_bg_color(portal_color, theme)
base_color = theme == 'dark' ? 'black' : 'white' base_color = theme == 'dark' ? 'black' : 'white'
"color-mix(in srgb, #{portal_color} 10%, #{base_color})" "color-mix(in srgb, #{portal_color} 20%, #{base_color})"
end end
def generate_portal_bg(portal_color, theme) def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg' bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}" "background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end end
def language_name(locale)
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
language_map[locale] || locale
end
def get_theme_names(theme)
if theme == 'light'
I18n.t('public_portal.header.appearance.light')
elsif theme == 'dark'
I18n.t('public_portal.header.appearance.dark')
else
I18n.t('public_portal.header.appearance.system')
end
end
def get_theme_icon(theme)
if theme == 'light'
'icons/sun'
elsif theme == 'dark'
'icons/moon'
else
'icons/monitor'
end
end
def generate_gradient_to_bottom(theme)
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
end
def generate_article_link(portal_slug, article_slug, theme)
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
end
def render_category_content(content)
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
end
end end

View File

@ -1,6 +1,7 @@
/* global axios */ /* global axios */
import PortalsAPI from './portals'; import PortalsAPI from './portals';
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
class ArticlesAPI extends PortalsAPI { class ArticlesAPI extends PortalsAPI {
constructor() { constructor() {
@ -12,14 +13,31 @@ class ArticlesAPI extends PortalsAPI {
portalSlug, portalSlug,
locale, locale,
status, status,
author_id, authorId,
category_slug, categorySlug,
sort,
}) { }) {
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`; const url = getArticleSearchURL({
if (status !== undefined) baseUrl += `&status=${status}`; pageNumber,
if (author_id) baseUrl += `&author_id=${author_id}`; portalSlug,
if (category_slug) baseUrl += `&category_slug=${category_slug}`; locale,
return axios.get(baseUrl); status,
authorId,
categorySlug,
sort,
host: this.url,
});
return axios.get(url);
}
searchArticles({ portalSlug, query }) {
const url = getArticleSearchURL({
portalSlug,
query,
host: this.url,
});
return axios.get(url);
} }
getArticle({ id, portalSlug }) { getArticle({ id, portalSlug }) {

View File

@ -17,6 +17,10 @@ class PortalsAPI extends ApiClient {
deletePortal(portalSlug) { deletePortal(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}`); return axios.delete(`${this.url}/${portalSlug}`);
} }
deleteLogo(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}/logo`);
}
} }
export default PortalsAPI; export default PortalsAPI;

View File

@ -26,9 +26,13 @@ export const buildCreatePayload = ({
payload.append('echo_id', echoId); payload.append('echo_id', echoId);
payload.append('cc_emails', ccEmails); payload.append('cc_emails', ccEmails);
payload.append('bcc_emails', bccEmails); payload.append('bcc_emails', bccEmails);
if (toEmails) { if (toEmails) {
payload.append('to_emails', toEmails); payload.append('to_emails', toEmails);
} }
if (contentAttributes) {
payload.append('content_attributes', JSON.stringify(contentAttributes));
}
} else { } else {
payload = { payload = {
content: message, content: message,

View File

@ -34,7 +34,7 @@ describe('#PortalAPI', () => {
portalSlug: 'room-rental', portalSlug: 'room-rental',
locale: 'en-US', locale: 'en-US',
status: 'published', status: 'published',
author_id: '1', authorId: '1',
}); });
expect(axiosMock.get).toHaveBeenCalledWith( expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1' '/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
@ -85,6 +85,33 @@ describe('#PortalAPI', () => {
window.axios = originalAxios; window.axios = originalAxios;
}); });
it('#searchArticles', () => {
articlesAPI.searchArticles({
query: 'test',
portalSlug: 'room-rental',
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?query=test'
);
});
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#updateArticle', () => { it('#updateArticle', () => {
articlesAPI.updateArticle({ articlesAPI.updateArticle({
articleId: 1, articleId: 1,

View File

@ -50,6 +50,7 @@ describe('#ConversationAPI', () => {
message: 'test content', message: 'test content',
echoId: 12, echoId: 12,
isPrivate: true, isPrivate: true,
contentAttributes: { in_reply_to: 12 },
files: [new Blob(['test-content'], { type: 'application/pdf' })], files: [new Blob(['test-content'], { type: 'application/pdf' })],
}); });
expect(formPayload).toBeInstanceOf(FormData); expect(formPayload).toBeInstanceOf(FormData);
@ -57,6 +58,10 @@ describe('#ConversationAPI', () => {
expect(formPayload.get('echo_id')).toEqual('12'); expect(formPayload.get('echo_id')).toEqual('12');
expect(formPayload.get('private')).toEqual('true'); expect(formPayload.get('private')).toEqual('true');
expect(formPayload.get('cc_emails')).toEqual(''); expect(formPayload.get('cc_emails')).toEqual('');
expect(formPayload.get('bcc_emails')).toEqual('');
expect(formPayload.get('content_attributes')).toEqual(
'{"in_reply_to":12}'
);
}); });
it('builds object payload if file is not available', () => { it('builds object payload if file is not available', () => {

View File

@ -1,9 +1,5 @@
@import '~vue2-datepicker/scss/index'; @import '~vue2-datepicker/scss/index';
.mx-datepicker-popup {
@apply z-[99999];
}
.date-picker { .date-picker {
&.no-margin { &.no-margin {
.mx-input { .mx-input {
@ -29,10 +25,52 @@
.mx-input[readonly] { .mx-input[readonly] {
@apply bg-white dark:bg-slate-900 cursor-pointer; @apply bg-white dark:bg-slate-900 cursor-pointer;
} }
.mx-icon-calendar {
@apply dark:text-slate-500;
}
} }
.mx-calendar-content .cell:hover { .mx-datepicker-main {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100; @apply border-0 bg-white dark:bg-slate-800;
.cell {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
&:hover,
&.hover-in-range,
&.in-range {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
}
}
.mx-time {
@apply border-0 bg-white dark:bg-slate-800;
.mx-time-header {
@apply border-0;
}
.mx-time-item {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
&:hover {
@apply bg-slate-75 dark:bg-slate-700;
}
}
}
.today {
@apply font-semibold;
}
}
.mx-datepicker-popup {
@apply z-[99999];
} }
.mx-datepicker-inline { .mx-datepicker-inline {
@ -41,24 +79,4 @@
.mx-calendar { .mx-calendar {
@apply w-full; @apply w-full;
} }
.cell.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
.mx-time-item.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
.today {
@apply font-semibold;
}
.mx-datepicker-main {
@apply border-0 bg-white dark:bg-slate-800;
}
.mx-time-header {
@apply border-0;
}
} }

View File

@ -16,3 +16,23 @@
width: 100%; width: 100%;
} }
} }
.integration-hooks {
.formulate-input[data-type='checkbox'] {
.formulate-input-wrapper {
@apply flex;
.formulate-input-element {
@apply pr-2;
input {
@apply mb-0;
}
}
}
.formulate-input-element-decorator {
@apply hidden;
}
}
}

View File

@ -207,6 +207,7 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue'; import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin'; import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries'; import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper'; import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@ -238,6 +239,7 @@ export default {
eventListenerMixins, eventListenerMixins,
alertMixin, alertMixin,
filterMixin, filterMixin,
uiSettingsMixin,
], ],
props: { props: {
conversationInbox: { conversationInbox: {
@ -408,8 +410,12 @@ export default {
}, },
conversationListPagination() { conversationListPagination() {
const conversationsPerPage = 25; const conversationsPerPage = 25;
const hasChatsOnView =
this.chatsOnView &&
Array.isArray(this.chatsOnView) &&
!this.chatsOnView.length;
const isNoFiltersOrFoldersAndChatListNotEmpty = const isNoFiltersOrFoldersAndChatListNotEmpty =
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== []; !this.hasAppliedFiltersOrActiveFolders && hasChatsOnView;
const isUnderPerPage = const isUnderPerPage =
this.chatsOnView.length < conversationsPerPage && this.chatsOnView.length < conversationsPerPage &&
this.activeAssigneeTabCount < conversationsPerPage && this.activeAssigneeTabCount < conversationsPerPage &&
@ -515,6 +521,7 @@ export default {
}, },
}, },
mounted() { mounted() {
this.setFiltersFromUISettings();
this.$store.dispatch('setChatStatusFilter', this.activeStatus); this.$store.dispatch('setChatStatusFilter', this.activeStatus);
this.$store.dispatch('setChatSortFilter', this.activeSortBy); this.$store.dispatch('setChatSortFilter', this.activeSortBy);
this.resetAndFetchData(); this.resetAndFetchData();
@ -544,6 +551,12 @@ export default {
this.$store.dispatch('customViews/update', payloadData); this.$store.dispatch('customViews/update', payloadData);
this.closeAdvanceFiltersModal(); this.closeAdvanceFiltersModal();
}, },
setFiltersFromUISettings() {
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
const { status, order_by: orderBy } = filterBy;
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
},
onClickOpenAddFoldersModal() { onClickOpenAddFoldersModal() {
this.showAddFoldersModal = true; this.showAddFoldersModal = true;
}, },

View File

@ -136,6 +136,7 @@ export default {
toStateName: 'labels_list', toStateName: 'labels_list',
showModalForNewItem: true, showModalForNewItem: true,
modalName: 'AddLabel', modalName: 'AddLabel',
dataTestid: 'sidebar-new-label-button',
children: this.labels.map(label => ({ children: this.labels.map(label => ({
id: label.id, id: label.id,
label: label.title, label: label.title,

View File

@ -78,6 +78,7 @@
variant="clear" variant="clear"
color-scheme="secondary" color-scheme="secondary"
icon="add" icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)" @click="e => newLinkClick(e, navigate)"
> >
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }} {{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}

View File

@ -61,6 +61,7 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
}), }),
isAICTAModalDismissed() { isAICTAModalDismissed() {
return this.uiSettings.is_open_ai_cta_modal_dismissed; return this.uiSettings.is_open_ai_cta_modal_dismissed;
@ -70,7 +71,8 @@ export default {
return ( return (
this.isAdmin && this.isAdmin &&
!this.isAIIntegrationEnabled && !this.isAIIntegrationEnabled &&
!this.isAICTAModalDismissed !this.isAICTAModalDismissed &&
this.isAChatwootInstance
); );
}, },
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal. // Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.

View File

@ -135,6 +135,7 @@ export default {
// allowSignature is a kill switch, ensuring no signature methods // allowSignature is a kill switch, ensuring no signature methods
// are triggered except when this flag is true // are triggered except when this flag is true
allowSignature: { type: Boolean, default: false }, allowSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
}, },
data() { data() {
@ -266,8 +267,11 @@ export default {
sendWithSignature() { sendWithSignature() {
// this is considered the source of truth, we watch this property // this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor // on change, we toggle the signature in the editor
const { send_with_signature: isEnabled } = this.uiSettings; if (this.allowSignature && !this.isPrivate && this.channelType) {
return isEnabled && this.allowSignature && !this.isPrivate; return this.fetchSignatureFlagFromUiSettings(this.channelType);
}
return false;
}, },
}, },
watch: { watch: {
@ -442,7 +446,7 @@ export default {
this.onFocus(); this.onFocus();
}, },
click: () => { click: () => {
// this.isEditorMouseFocusedOnAnImage(); Enable it when the backend supports for message resize is done. this.isEditorMouseFocusedOnAnImage();
}, },
blur: () => { blur: () => {
this.onBlur(); this.onBlur();
@ -670,7 +674,7 @@ export default {
() => this.resetTyping(), () => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME TYPING_INDICATOR_IDLE_TIME
); );
// this.updateImgToolbarOnDelete(); Enable it when the backend supports for message resize is done. this.updateImgToolbarOnDelete();
}, },
onKeydown(event) { onKeydown(event) {
if (this.isEnterToSendEnabled()) { if (this.isEnterToSendEnabled()) {

View File

@ -101,7 +101,7 @@
<transition name="modal-fade"> <transition name="modal-fade">
<div <div
v-show="$refs.upload && $refs.upload.dropActive" v-show="$refs.upload && $refs.upload.dropActive"
class="flex items-center justify-center gap-2 fixed left-0 right-0 top-0 bottom-0 w-full h-full z-20 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col" class="fixed top-0 bottom-0 left-0 right-0 z-20 flex flex-col items-center justify-center w-full h-full gap-2 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent"
> >
<fluent-icon icon="cloud-backup" size="40" /> <fluent-icon icon="cloud-backup" size="40" />
<h4 class="page-sub-title text-slate-600 dark:text-slate-200"> <h4 class="page-sub-title text-slate-600 dark:text-slate-200">
@ -109,6 +109,16 @@
</h4> </h4>
</div> </div>
</transition> </transition>
<woot-button
v-if="enableInsertArticleInReply"
v-tooltip.top-end="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
icon="document-text-link"
color-scheme="secondary"
variant="smooth"
size="small"
:title="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
@click="toggleInsertArticle"
/>
</div> </div>
<div class="right-wrap"> <div class="right-wrap">
<woot-button <woot-button
@ -233,6 +243,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
portalSlug: {
type: String,
required: true,
},
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@ -299,14 +313,21 @@ export default {
return !this.isOnPrivateNote; return !this.isOnPrivateNote;
}, },
sendWithSignature() { sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings; // channelType is sourced from inboxMixin
return isEnabled; return this.fetchSignatureFlagFromUiSettings(this.channelType);
}, },
signatureToggleTooltip() { signatureToggleTooltip() {
return this.sendWithSignature return this.sendWithSignature
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP') ? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP'); : this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
}, },
enableInsertArticleInReply() {
const isFeatEnabled = this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
);
return isFeatEnabled && this.portalSlug;
},
}, },
mounted() { mounted() {
ActiveStorage.start(); ActiveStorage.start();
@ -318,13 +339,14 @@ export default {
} }
}, },
toggleMessageSignature() { toggleMessageSignature() {
this.updateUISettings({ this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
send_with_signature: !this.sendWithSignature,
});
}, },
replaceText(text) { replaceText(text) {
this.$emit('replace-text', text); this.$emit('replace-text', text);
}, },
toggleInsertArticle() {
this.$emit('toggle-insert-article');
},
}, },
}; };
</script> </script>

View File

@ -32,7 +32,7 @@
}}</span> }}</span>
<filter-item <filter-item
type="sort" type="sort"
:selected-value="chatSortFilter" :selected-value="sortFilter"
:items="chatSortItems" :items="chatSortItems"
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS" path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
@onChangeFilter="onChangeFilter" @onChangeFilter="onChangeFilter"
@ -47,12 +47,13 @@ import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import FilterItem from './FilterItem.vue'; import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default { export default {
components: { components: {
FilterItem, FilterItem,
}, },
mixins: [clickaway], mixins: [clickaway, uiSettingsMixin],
data() { data() {
return { return {
showActionsDropdown: false, showActionsDropdown: false,
@ -83,8 +84,17 @@ export default {
closeDropdown() { closeDropdown() {
this.showActionsDropdown = false; this.showActionsDropdown = false;
}, },
onChangeFilter(type, value) { onChangeFilter(value, type) {
this.$emit('changeFilter', type, value); this.$emit('changeFilter', value, type);
this.saveSelectedFilter(type, value);
},
saveSelectedFilter(type, value) {
this.updateUISettings({
conversations_filter_by: {
status: type === 'status' ? value : this.chatStatus,
order_by: type === 'sort' ? value : this.sortFilter,
},
});
}, },
}, },
}; };

View File

@ -22,22 +22,12 @@
:bcc="emailHeadAttributes.bcc" :bcc="emailHeadAttributes.bcc"
:is-incoming="isIncoming" :is-incoming="isIncoming"
/> />
<blockquote v-if="storyReply" class="story-reply-quote"> <instagram-story-reply v-if="storyUrl" :story-url="storyUrl" />
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<bubble-image
v-if="!hasImgStoryError && storyUrl"
:url="storyUrl"
@error="onStoryLoadError"
/>
<bubble-video
v-else-if="hasImgStoryError && storyUrl"
:url="storyUrl"
/>
</blockquote>
<bubble-reply-to <bubble-reply-to
v-if="inReplyToMessageId && inboxSupportsReplyTo" v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
:message="inReplyTo" :message="inReplyTo"
:message-type="data.message_type" :message-type="data.message_type"
:parent-has-attachments="hasAttachments"
/> />
<bubble-text <bubble-text
v-if="data.content" v-if="data.content"
@ -58,10 +48,15 @@
</span> </span>
<div v-if="!isPending && hasAttachments"> <div v-if="!isPending && hasAttachments">
<div v-for="attachment in attachments" :key="attachment.id"> <div v-for="attachment in attachments" :key="attachment.id">
<instagram-story
v-if="isAnInstagramStory"
:story-url="attachment.data_url"
@error="onMediaLoadError"
/>
<bubble-image-audio-video <bubble-image-audio-video
v-if="isAttachmentImageVideoAudio(attachment.file_type)" v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
:attachment="attachment" :attachment="attachment"
@error="onImageLoadError" @error="onMediaLoadError"
/> />
<bubble-location <bubble-location
v-else-if="attachment.file_type === 'location'" v-else-if="attachment.file_type === 'location'"
@ -74,9 +69,6 @@
:name="data.content" :name="data.content"
:phone-number="attachment.fallback_title" :phone-number="attachment.fallback_title"
/> />
<instagram-image-error-placeholder
v-else-if="hasImageError && hasInstagramStory"
/>
<bubble-file v-else :url="attachment.data_url" /> <bubble-file v-else :url="attachment.data_url" />
</div> </div>
</div> </div>
@ -88,7 +80,6 @@
:story-id="`${storyId}`" :story-id="`${storyId}`"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel" :is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-email="isEmailContentType" :is-email="isEmailContentType"
:is-private="data.private" :is-private="data.private"
:message-type="data.message_type" :message-type="data.message_type"
@ -137,19 +128,18 @@
<script> <script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import BubbleActions from './bubble/Actions.vue'; import BubbleActions from './bubble/Actions.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleFile from './bubble/File.vue'; import BubbleFile from './bubble/File.vue';
import BubbleImage from './bubble/Image.vue';
import BubbleVideo from './bubble/Video.vue';
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue'; import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
import BubbleIntegration from './bubble/Integration.vue'; import BubbleIntegration from './bubble/Integration.vue';
import BubbleLocation from './bubble/Location.vue'; import BubbleLocation from './bubble/Location.vue';
import BubbleMailHead from './bubble/MailHead.vue'; import BubbleMailHead from './bubble/MailHead.vue';
import BubbleText from './bubble/Text.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue'; import BubbleReplyTo from './bubble/ReplyTo.vue';
import Spinner from 'shared/components/Spinner.vue'; import BubbleText from './bubble/Text.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue'; import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue'; import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import Spinner from 'shared/components/Spinner.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin'; import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
@ -162,19 +152,18 @@ import { LocalStorage } from 'shared/helpers/localStorage';
export default { export default {
components: { components: {
BubbleActions, BubbleActions,
BubbleContact,
BubbleFile, BubbleFile,
BubbleImage,
BubbleVideo,
BubbleImageAudioVideo, BubbleImageAudioVideo,
BubbleIntegration, BubbleIntegration,
BubbleLocation, BubbleLocation,
BubbleMailHead, BubbleMailHead,
BubbleText,
BubbleContact,
BubbleReplyTo, BubbleReplyTo,
BubbleText,
ContextMenu, ContextMenu,
InstagramStory,
InstagramStoryReply,
Spinner, Spinner,
instagramImageErrorPlaceholder,
}, },
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin], mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
props: { props: {
@ -190,17 +179,13 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
hasInstagramStory: {
type: Boolean,
default: false,
},
isWebWidgetInbox: { isWebWidgetInbox: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
inboxSupportsReplyTo: { inboxSupportsReplyTo: {
type: Boolean, type: Object,
default: false, default: () => ({}),
}, },
inReplyTo: { inReplyTo: {
type: Object, type: Object,
@ -210,10 +195,9 @@ export default {
data() { data() {
return { return {
showContextMenu: false, showContextMenu: false,
hasImageError: false, hasMediaLoadError: false,
contextMenuPosition: {}, contextMenuPosition: {},
showBackgroundHighlight: false, showBackgroundHighlight: false,
hasImgStoryError: false,
}; };
}, },
computed: { computed: {
@ -289,19 +273,22 @@ export default {
// since old messages are only loaded when the user scrolls up // since old messages are only loaded when the user scrolls up
return this.data.content_attributes?.in_reply_to; return this.data.content_attributes?.in_reply_to;
}, },
isAnInstagramStory() {
return this.contentAttributes.image_type === 'story_mention';
},
contextMenuEnabledOptions() { contextMenuEnabledOptions() {
return { return {
copy: this.hasText, copy: this.hasText,
delete: this.hasText || this.hasAttachments, delete: this.hasText || this.hasAttachments,
cannedResponse: this.isOutgoing && this.hasText, cannedResponse: this.isOutgoing && this.hasText,
replyTo: !this.data.private && this.inboxSupportsReplyTo, replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
}; };
}, },
contentAttributes() { contentAttributes() {
return this.data.content_attributes || {}; return this.data.content_attributes || {};
}, },
externalError() { externalError() {
return this.contentAttributes.external_error || null; return this.contentAttributes.external_error || '';
}, },
sender() { sender() {
return this.data.sender || {}; return this.data.sender || {};
@ -318,9 +305,6 @@ export default {
storyUrl() { storyUrl() {
return this.contentAttributes.story_url || null; return this.contentAttributes.story_url || null;
}, },
storyReply() {
return this.storyUrl && this.hasInstagramStory;
},
contentType() { contentType() {
const { const {
data: { content_type: contentType }, data: { content_type: contentType },
@ -401,7 +385,7 @@ export default {
return false; return false;
} }
if (this.isFailed) { if (this.isFailed) {
return this.externalError ? '' : this.$t(`CONVERSATION.SEND_FAILED`); return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
} }
return false; return false;
}, },
@ -459,13 +443,11 @@ export default {
}, },
watch: { watch: {
data() { data() {
this.hasImageError = false; this.hasMediaLoadError = false;
this.hasImgStoryError = false;
}, },
}, },
mounted() { mounted() {
this.hasImageError = false; this.hasMediaLoadError = false;
this.hasImgStoryError = false;
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu); bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer(); this.setupHighlightTimer();
}, },
@ -475,13 +457,13 @@ export default {
}, },
methods: { methods: {
isAttachmentImageVideoAudio(fileType) { isAttachmentImageVideoAudio(fileType) {
return ['image', 'audio', 'video'].includes(fileType); return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
}, },
hasMediaAttachment(type) { hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) { if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data; const { attachments = [{}] } = this.data;
const { file_type: fileType } = attachments[0]; const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasImageError; return fileType === type && !this.hasMediaLoadError;
} }
if (this.storyReply) { if (this.storyReply) {
return true; return true;
@ -494,11 +476,8 @@ export default {
async retrySendMessage() { async retrySendMessage() {
await this.$store.dispatch('sendMessageWithData', this.data); await this.$store.dispatch('sendMessageWithData', this.data);
}, },
onImageLoadError() { onMediaLoadError() {
this.hasImageError = true; this.hasMediaLoadError = true;
},
onStoryLoadError() {
this.hasImgStoryError = true;
}, },
openContextMenu(e) { openContextMenu(e) {
const shouldSkipContextMenu = const shouldSkipContextMenu =
@ -730,8 +709,4 @@ li.right {
} }
} }
} }
.story-reply-quote {
@apply mt-2 mx-4 mb-0 px-2 pb-0 pt-2 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200;
}
</style> </style>

View File

@ -31,7 +31,6 @@
:data="message" :data="message"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel" :is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox" :is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo" :inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)" :in-reply-to="getInReplyToMessage(message)"
@ -54,7 +53,6 @@
:data="message" :data="message"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel" :is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox" :is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo" :inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)" :in-reply-to="getInReplyToMessage(message)"
@ -116,7 +114,6 @@ import { LocalStorage } from 'shared/helpers/localStorage';
// constants // constants
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { REPLY_POLICY } from 'shared/constants/links'; import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals'; import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@ -238,10 +235,6 @@ export default {
isATweet() { isATweet() {
return this.conversationType === 'tweet'; return this.conversationType === 'tweet';
}, },
hasInstagramStory() {
return this.conversationType === 'instagram_direct_message';
},
isRightOrLeftIcon() { isRightOrLeftIcon() {
if (this.isContactPanelOpen) { if (this.isContactPanelOpen) {
return 'arrow-chevron-right'; return 'arrow-chevron-right';
@ -291,13 +284,10 @@ export default {
return this.currentChat.unread_count || 0; return this.currentChat.unread_count || 0;
}, },
inboxSupportsReplyTo() { inboxSupportsReplyTo() {
return ( return {
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) && incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
this.isFeatureEnabledonAccount( outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
this.accountId, };
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
);
}, },
}, },
@ -350,7 +340,7 @@ export default {
// method available in mixin, need to ensure that integrations are present // method available in mixin, need to ensure that integrations are present
await this.fetchIntegrationsIfRequired(); await this.fetchIntegrationsIfRequired();
if (!this.isAIIntegrationEnabled) { if (!this.isLabelSuggestionFeatureEnabled) {
return; return;
} }

View File

@ -18,6 +18,12 @@
:popout-reply-box="popoutReplyBox" :popout-reply-box="popoutReplyBox"
@click="$emit('click')" @click="$emit('click')"
/> />
<article-search-popover
v-if="showArticleSearchPopover && connectedPortalSlug"
:selected-portal-slug="connectedPortalSlug"
@insert="handleInsert"
@close="onSearchPopoverClose"
/>
<div class="reply-box__top"> <div class="reply-box__top">
<reply-to-message <reply-to-message
v-if="shouldShowReplyToMessage" v-if="shouldShowReplyToMessage"
@ -35,7 +41,7 @@
v-if="showEmojiPicker" v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker" v-on-clickaway="hideEmojiPicker"
:class="emojiDialogClassOnExpandedLayoutAndRTLView" :class="emojiDialogClassOnExpandedLayoutAndRTLView"
:on-click="emojiOnClick" :on-click="addIntoEditor"
/> />
<reply-email-head <reply-email-head
v-if="showReplyHead" v-if="showReplyHead"
@ -79,6 +85,7 @@
:variables="messageVariables" :variables="messageVariables"
:signature="signatureToApply" :signature="signatureToApply"
:allow-signature="true" :allow-signature="true"
:channel-type="channelType"
@typing-off="onTypingOff" @typing-off="onTypingOff"
@typing-on="onTypingOn" @typing-on="onTypingOn"
@focus="onFocus" @focus="onFocus"
@ -121,10 +128,12 @@
:toggle-audio-recorder="toggleAudioRecorder" :toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker" :toggle-emoji-picker="toggleEmojiPicker"
:message="message" :message="message"
:portal-slug="connectedPortalSlug"
:new-conversation-modal-active="newConversationModalActive" :new-conversation-modal-active="newConversationModalActive"
@selectWhatsappTemplate="openWhatsappTemplateModal" @selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor" @toggle-editor="toggleRichContentEditor"
@replace-text="replaceText" @replace-text="replaceText"
@toggle-insert-article="toggleInsertArticle"
/> />
<whatsapp-templates <whatsapp-templates
:inbox-id="inbox.id" :inbox-id="inbox.id"
@ -154,6 +163,7 @@ import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.v
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue'; import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import ReplyEmailHead from './ReplyEmailHead.vue'; import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue'; import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert'; import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
import Banner from 'dashboard/components/ui/Banner.vue'; import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants'; import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
@ -184,7 +194,6 @@ import {
replaceSignature, replaceSignature,
extractTextFromMarkdown, extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper'; } from 'dashboard/helper/editorHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage'; import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage'; import { LocalStorage } from 'shared/helpers/localStorage';
@ -206,6 +215,7 @@ export default {
Banner, Banner,
WhatsappTemplates, WhatsappTemplates,
MessageSignatureMissingAlert, MessageSignatureMissingAlert,
ArticleSearchPopover,
}, },
mixins: [ mixins: [
clickaway, clickaway,
@ -248,6 +258,7 @@ export default {
showCannedMenu: false, showCannedMenu: false,
showVariablesMenu: false, showVariablesMenu: false,
newConversationModalActive: false, newConversationModalActive: false,
showArticleSearchPopover: false,
}; };
}, },
computed: { computed: {
@ -264,11 +275,7 @@ export default {
return ( return (
this.inReplyTo?.id && this.inReplyTo?.id &&
!this.isPrivate && !this.isPrivate &&
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) && this.inboxHasFeature(INBOX_FEATURES.REPLY_TO)
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
); );
}, },
showRichContentEditor() { showRichContentEditor() {
@ -466,8 +473,7 @@ export default {
return !!this.signatureToApply; return !!this.signatureToApply;
}, },
sendWithSignature() { sendWithSignature() {
const { send_with_signature: isEnabled } = this.uiSettings; return this.fetchSignatureFlagFromUiSettings(this.channelType);
return isEnabled;
}, },
editorMessageKey() { editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings; const { editor_message_key: isEnabled } = this.uiSettings;
@ -506,11 +512,18 @@ export default {
? this.messageSignature ? this.messageSignature
: extractTextFromMarkdown(this.messageSignature); : extractTextFromMarkdown(this.messageSignature);
}, },
connectedPortalSlug() {
const { help_center: portal = {} } = this.inbox;
const { slug = '' } = portal;
return slug;
},
}, },
watch: { watch: {
currentChat(conversation) { currentChat(conversation) {
const { can_reply: canReply } = conversation; const { can_reply: canReply } = conversation;
this.setCCAndToEmailsFromLastChat();
if (this.isOnPrivateNote) { if (this.isOnPrivateNote) {
return; return;
} }
@ -521,7 +534,6 @@ export default {
this.replyType = REPLY_EDITOR_MODES.NOTE; this.replyType = REPLY_EDITOR_MODES.NOTE;
} }
this.setCCAndToEmailsFromLastChat();
this.fetchAndSetReplyTo(); this.fetchAndSetReplyTo();
}, },
conversationIdByRoute(conversationId, oldConversationId) { conversationIdByRoute(conversationId, oldConversationId) {
@ -531,17 +543,25 @@ export default {
} }
}, },
message(updatedMessage) { message(updatedMessage) {
this.hasSlashCommand = // Check if the message starts with a slash.
updatedMessage[0] === '/' && !this.showRichContentEditor; const bodyWithoutSignature = removeSignature(
const hasNextWord = updatedMessage.includes(' '); updatedMessage,
const isShortCodeActive = this.hasSlashCommand && !hasNextWord; this.signatureToApply
if (isShortCodeActive) { );
this.mentionSearchKey = updatedMessage.substring(1); const startsWithSlash = bodyWithoutSignature.startsWith('/');
this.showMentions = true;
} else { // Determine if the user is potentially typing a slash command.
this.mentionSearchKey = ''; // This is true if the message starts with a slash and the rich content editor is not active.
this.showMentions = false; this.hasSlashCommand = startsWithSlash && !this.showRichContentEditor;
} this.showMentions = this.hasSlashCommand;
// If a slash command is active, extract the command text after the slash.
// If not, reset the mentionSearchKey.
this.mentionSearchKey = this.hasSlashCommand
? bodyWithoutSignature.substring(1)
: '';
// Autosave the current message draft.
this.doAutoSaveDraft(); this.doAutoSaveDraft();
}, },
replyType(updatedReplyType, oldReplyType) { replyType(updatedReplyType, oldReplyType) {
@ -588,6 +608,23 @@ export default {
); );
}, },
methods: { methods: {
handleInsert(article) {
const { url, title } = article;
if (this.isRichEditorEnabled) {
// Removing empty lines from the title
const lines = title.split('\n');
const nonEmptyLines = lines.filter(line => line.trim() !== '');
const filteredMarkdown = nonEmptyLines.join(' ');
bus.$emit(
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
`[${filteredMarkdown}](${url})`
);
} else {
this.addIntoEditor(
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
);
}
},
toggleRichContentEditor() { toggleRichContentEditor() {
this.updateUISettings({ this.updateUISettings({
display_rich_content_editor: !this.showRichContentEditor, display_rich_content_editor: !this.showRichContentEditor,
@ -819,10 +856,18 @@ export default {
this.hideWhatsappTemplatesModal(); this.hideWhatsappTemplatesModal();
}, },
replaceText(message) { replaceText(message) {
if (this.sendWithSignature && !this.private) {
// if signature is enabled, append it to the message
// appendSignature ensures that the signature is not duplicated
// so we don't need to check if the signature is already present
message = appendSignature(message, this.signatureToApply);
}
const updatedMessage = replaceVariablesInMessage({ const updatedMessage = replaceVariablesInMessage({
message, message,
variables: this.messageVariables, variables: this.messageVariables,
}); });
setTimeout(() => { setTimeout(() => {
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE); this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.message = updatedMessage; this.message = updatedMessage;
@ -845,22 +890,22 @@ export default {
clearEditorSelection() { clearEditorSelection() {
this.updateEditorSelectionWith = ''; this.updateEditorSelectionWith = '';
}, },
insertEmoji(emoji, selectionStart, selectionEnd) { insertIntoTextEditor(text, selectionStart, selectionEnd) {
const { message } = this; const { message } = this;
const newMessage = const newMessage =
message.slice(0, selectionStart) + message.slice(0, selectionStart) +
emoji + text +
message.slice(selectionEnd, message.length); message.slice(selectionEnd, message.length);
this.message = newMessage; this.message = newMessage;
}, },
emojiOnClick(emoji) { addIntoEditor(content) {
if (this.showRichContentEditor) { if (this.showRichContentEditor) {
this.updateEditorSelectionWith = emoji; this.updateEditorSelectionWith = content;
this.onFocus(); this.onFocus();
} }
if (!this.showRichContentEditor) { if (!this.showRichContentEditor) {
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el; const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
this.insertEmoji(emoji, selectionStart, selectionEnd); this.insertIntoTextEditor(content, selectionStart, selectionEnd);
} }
}, },
clearMessage() { clearMessage() {
@ -964,6 +1009,19 @@ export default {
(item, index) => itemIndex !== index (item, index) => itemIndex !== index
); );
}, },
setReplyToInPayload(payload) {
if (this.inReplyTo?.id) {
return {
...payload,
contentAttributes: {
...payload.contentAttributes,
in_reply_to: this.inReplyTo.id,
},
};
}
return payload;
},
getMessagePayloadForWhatsapp(message) { getMessagePayloadForWhatsapp(message) {
const multipleMessagePayload = []; const multipleMessagePayload = [];
@ -973,41 +1031,41 @@ export default {
const attachedFile = this.globalConfig.directUploadsEnabled const attachedFile = this.globalConfig.directUploadsEnabled
? attachment.blobSignedId ? attachment.blobSignedId
: attachment.resource.file; : attachment.resource.file;
const attachmentPayload = { let attachmentPayload = {
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
files: [attachedFile], files: [attachedFile],
private: false, private: false,
message: caption, message: caption,
sender: this.sender, sender: this.sender,
}; };
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
multipleMessagePayload.push(attachmentPayload); multipleMessagePayload.push(attachmentPayload);
caption = ''; caption = '';
}); });
} else { } else {
const messagePayload = { let messagePayload = {
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
message, message,
private: false, private: false,
sender: this.sender, sender: this.sender,
}; };
messagePayload = this.setReplyToInPayload(messagePayload);
multipleMessagePayload.push(messagePayload); multipleMessagePayload.push(messagePayload);
} }
return multipleMessagePayload; return multipleMessagePayload;
}, },
getMessagePayload(message) { getMessagePayload(message) {
const messagePayload = { let messagePayload = {
conversationId: this.currentChat.id, conversationId: this.currentChat.id,
message, message,
private: this.isPrivate, private: this.isPrivate,
sender: this.sender, sender: this.sender,
}; };
messagePayload = this.setReplyToInPayload(messagePayload);
if (this.inReplyTo?.id) {
messagePayload.contentAttributes = {
in_reply_to: this.inReplyTo.id,
};
}
if (this.attachedFiles && this.attachedFiles.length) { if (this.attachedFiles && this.attachedFiles.length) {
messagePayload.files = []; messagePayload.files = [];
@ -1106,6 +1164,12 @@ export default {
// When new conversation modal is open // When new conversation modal is open
this.newConversationModalActive = isActive; this.newConversationModalActive = isActive;
}, },
onSearchPopoverClose() {
this.showArticleSearchPopover = false;
},
toggleInsertArticle() {
this.showArticleSearchPopover = !this.showArticleSearchPopover;
},
}, },
}; };
</script> </script>
@ -1124,7 +1188,7 @@ export default {
} }
.reply-box { .reply-box {
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900; @apply relative border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
&.is-private { &.is-private {
@apply bg-yellow-50 dark:bg-yellow-200; @apply bg-yellow-50 dark:bg-yellow-200;

View File

@ -114,10 +114,6 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
hasInstagramStory: {
type: Boolean,
default: true,
},
messageType: { messageType: {
type: Number, type: Number,
default: 1, default: 1,
@ -196,28 +192,46 @@ export default {
if (!this.showStatusIndicators) { if (!this.showStatusIndicators) {
return false; return false;
} }
// Messages will be marked as sent for the Email channel if they have a source ID.
if (this.isAnEmailChannel) { if (this.isAnEmailChannel) {
return !!this.sourceId; return !!this.sourceId;
} }
if (this.isAWhatsAppChannel || this.isATwilioChannel) { if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox ||
this.isASmsInbox ||
this.isATelegramChannel
) {
return this.sourceId && this.isSent; return this.sourceId && this.isSent;
} }
// All messages will be mark as sent for the Line channel, as there is no source ID.
if (this.isALineChannel) {
return true;
}
return false; return false;
}, },
showDeliveredIndicator() { showDeliveredIndicator() {
if (!this.showStatusIndicators) { if (!this.showStatusIndicators) {
return false; return false;
} }
if (
if (this.isAWhatsAppChannel || this.isATwilioChannel) { this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isASmsInbox ||
this.isAFacebookInbox
) {
return this.sourceId && this.isDelivered; return this.sourceId && this.isDelivered;
} }
// We will consider messages as delivered for web widget inbox if they are sent // All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (this.isAWebWidgetInbox) { if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isSent; return this.isSent;
} }
if (this.isALineChannel) {
return this.isDelivered;
}
return false; return false;
}, },
@ -225,14 +239,16 @@ export default {
if (!this.showStatusIndicators) { if (!this.showStatusIndicators) {
return false; return false;
} }
if (
if (this.isAWebWidgetInbox || this.isAPIInbox) { this.isAWhatsAppChannel ||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat; this.isATwilioChannel ||
return contactLastSeenAt >= this.createdAt; this.isAFacebookInbox
) {
return this.sourceId && this.isRead;
} }
if (this.isAWhatsAppChannel || this.isATwilioChannel) { if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.sourceId && this.isRead; return this.isRead;
} }
return false; return false;

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="image message-text__wrap"> <div class="image message-text__wrap">
<img :src="url" @click="onClick" @error="onImgError()" /> <img :src="url" @click="onClick" @error="$emit('error')" />
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose"> <woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
<img :src="url" class="modal-image skip-context-menu" /> <img :src="url" class="modal-image skip-context-menu" />
</woot-modal> </woot-modal>
@ -28,9 +28,6 @@ export default {
onClick() { onClick() {
this.show = true; this.show = true;
}, },
onImgError() {
this.$emit('error');
},
}, },
}; };
</script> </script>

View File

@ -4,13 +4,14 @@
v-if="isImage && !isImageError" v-if="isImage && !isImageError"
:src="attachment.data_url" :src="attachment.data_url"
@click="onClick" @click="onClick"
@error="onImgError()" @error="onImgError"
/> />
<video <video
v-if="isVideo" v-if="isVideo"
:src="attachment.data_url" :src="attachment.data_url"
muted muted
playsInline playsInline
@error="onImgError"
@click="onClick" @click="onClick"
/> />
<audio v-else-if="isAudio" controls class="skip-context-menu"> <audio v-else-if="isAudio" controls class="skip-context-menu">
@ -21,7 +22,7 @@
:show.sync="show" :show.sync="show"
:attachment="attachment" :attachment="attachment"
:all-attachments="filteredCurrentChatAttachments" :all-attachments="filteredCurrentChatAttachments"
@error="onImgError()" @error="onImgError"
@close="onClose" @close="onClose"
/> />
</div> </div>

View File

@ -0,0 +1,51 @@
<template>
<bubble-image
v-if="!hasImgStoryError"
:url="storyUrl"
@error="onImageLoadError"
/>
<bubble-video
v-else-if="!hasVideoStoryError"
:url="storyUrl"
@error="onVideoLoadError"
/>
<instagram-story-error-place-holder v-else />
</template>
<script>
import BubbleImage from './Image.vue';
import BubbleVideo from './Video.vue';
import InstagramStoryErrorPlaceHolder from './InstagramStoryErrorPlaceHolder.vue';
export default {
components: {
BubbleImage,
BubbleVideo,
InstagramStoryErrorPlaceHolder,
},
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
this.emitError();
},
onVideoLoadError() {
this.hasVideoStoryError = true;
this.emitError();
},
emitError() {
this.$emit('error');
},
},
};
</script>

View File

@ -0,0 +1,14 @@
<template>
<div
class="flex items-center justify-center px-8 h-28 w-full bg-slate-100 text-slate-700 dark:bg-slate-500 dark:text-slate-75"
>
<fluent-icon icon="document-error" size="32" />
<p class="mb-0 text-slate-700 dark:text-slate-75">
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
</p>
</div>
</template>
<script>
export default {};
</script>

View File

@ -0,0 +1,35 @@
<template>
<blockquote
class="my-0 px-2 pb-0 pt-0 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200"
>
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<instagram-story :story-url="storyUrl" class="mt-3 rounded-md" />
</blockquote>
</template>
<script>
import InstagramStory from './InstagramStory.vue';
export default {
components: { InstagramStory },
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
},
onVideoLoadError() {
this.hasVideoStoryError = true;
},
},
};
</script>

View File

@ -1,10 +1,11 @@
<template> <template>
<div <div
class="px-2 py-1.5 -mx-2 rounded-sm min-w-[15rem] mb-2" class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
:class="{ :class="{
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50': 'bg-slate-50 dark:bg-slate-600 dark:text-slate-50':
messageType === MESSAGE_TYPE.INCOMING, messageType === MESSAGE_TYPE.INCOMING,
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING, 'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
'-mx-2': !parentHasAttachments,
}" }"
> >
<message-preview <message-preview
@ -33,6 +34,10 @@ export default {
type: Number, type: Number,
required: true, required: true,
}, },
parentHasAttachments: {
type: Boolean,
required: true,
},
}, },
data() { data() {
return { MESSAGE_TYPE }; return { MESSAGE_TYPE };

View File

@ -1,12 +1,12 @@
<template> <template>
<div class="video message-text__wrap"> <div class="video message-text__wrap">
<video :src="url" muted playsInline @click="onClick" /> <video ref="videoElement" :src="url" muted playsInline @click="onClick" />
<woot-modal :show.sync="show" :on-close="onClose"> <woot-modal :show.sync="show" :on-close="onClose">
<video <video
:src="url" :src="url"
controls controls
playsInline playsInline
class="modal-video skip-context-menu" class="modal-video skip-context-menu mx-auto"
/> />
</woot-modal> </woot-modal>
</div> </div>
@ -25,6 +25,11 @@ export default {
show: false, show: false,
}; };
}, },
mounted() {
this.$refs.videoElement.onerror = () => {
this.$emit('error');
};
},
methods: { methods: {
onClose() { onClose() {
this.show = false; this.show = false;

View File

@ -1,26 +0,0 @@
<template>
<div class="image-placeholder">
<fluent-icon icon="document-error" size="32" />
<p>{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}</p>
</div>
</template>
<script>
export default {};
</script>
<style scoped>
.image-placeholder {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: var(--space-slab);
height: calc(var(--space-large) * 10);
margin-top: var(--space-one);
width: 100%;
background-color: var(--s-75);
color: var(--s-800);
border-radius: var(--border-radius-normal);
}
</style>

View File

@ -16,5 +16,5 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management', TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder', VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs', AUDIT_LOGS: 'audit_logs',
MESSAGE_REPLY_TO: 'message_reply_to', INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
}; };

View File

@ -3,6 +3,7 @@ export const CONVERSATION_EVENTS = Object.freeze({
SENT_MESSAGE: 'Sent a message', SENT_MESSAGE: 'Sent a message',
SENT_PRIVATE_NOTE: 'Sent a private note', SENT_PRIVATE_NOTE: 'Sent a private note',
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response', INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
TRANSLATE_A_MESSAGE: 'Translated a message',
INSERTED_A_VARIABLE: 'Inserted a variable', INSERTED_A_VARIABLE: 'Inserted a variable',
USED_MENTIONS: 'Used mentions', USED_MENTIONS: 'Used mentions',
SEARCH_CONVERSATION: 'Searched conversations', SEARCH_CONVERSATION: 'Searched conversations',

View File

@ -64,3 +64,47 @@ export const isValidURL = value => {
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm; /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
return URL_REGEX.test(value); return URL_REGEX.test(value);
}; };
export const getArticleSearchURL = ({
host,
portalSlug,
pageNumber,
locale,
status,
authorId,
categorySlug,
sort,
query,
}) => {
const queryParams = new URLSearchParams({});
const params = {
page: pageNumber,
locale,
status,
author_id: authorId,
category_slug: categorySlug,
sort,
query,
};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
queryParams.set(key, value);
}
});
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
};
export const hasValidAvatarUrl = avatarUrl => {
try {
const { host: avatarUrlHost } = new URL(avatarUrl);
const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
avatarUrlHost
);
return avatarUrl && !isFromGravatar;
} catch (error) {
return false;
}
};

View File

@ -135,6 +135,7 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
assign_team: teams, assign_team: teams,
send_email_to_team: teams, send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'), add_label: generateConditionOptions(labels, 'title'),
remove_label: generateConditionOptions(labels, 'title'),
change_priority: PRIORITY_CONDITION_VALUES, change_priority: PRIORITY_CONDITION_VALUES,
}; };
return actionsMap[type]; return actionsMap[type];

View File

@ -3,6 +3,8 @@ import {
conversationUrl, conversationUrl,
isValidURL, isValidURL,
conversationListPageURL, conversationListPageURL,
getArticleSearchURL,
hasValidAvatarUrl,
} from '../URLHelper'; } from '../URLHelper';
describe('#URL Helpers', () => { describe('#URL Helpers', () => {
@ -75,4 +77,117 @@ describe('#URL Helpers', () => {
expect(isValidURL('alert.window')).toBe(false); expect(isValidURL('alert.window')).toBe(false);
}); });
}); });
describe('getArticleSearchURL', () => {
it('should generate a basic URL without optional parameters', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
it('should include status parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
status: 'published',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&status=published'
);
});
it('should include author_id parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
authorId: 123,
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&author_id=123'
);
});
it('should include category_slug parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
categorySlug: 'technology',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&category_slug=technology'
);
});
it('should include sort parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
sort: 'views',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en&sort=views');
});
it('should handle multiple optional parameters', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
status: 'draft',
authorId: 456,
categorySlug: 'science',
sort: 'views',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&status=draft&author_id=456&category_slug=science&sort=views'
);
});
it('should handle missing optional parameters gracefully', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
});
describe('hasValidAvatarUrl', () => {
test('should return true for valid non-Gravatar URL', () => {
expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
});
test('should return false for a Gravatar URL (www.gravatar.com)', () => {
expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
false
);
});
test('should return false for a Gravatar URL (gravatar)', () => {
expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
});
test('should handle invalid URL', () => {
expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
});
test('should return false for empty or undefined URL', () => {
expect(hasValidAvatarUrl('')).toBe(false);
expect(hasValidAvatarUrl()).toBe(false);
});
});
}); });

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "ابحث في الايموجي", "PLACEHOLDER": "ابحث في الايموجي",
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك" "NOT_FOUND": "لا يوجد إيموجي يطابق بحثك",
"REMOVE": "حذف"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "الاسم", "LABEL": "الاسم",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "الاسم مطلوب" "ERROR": "الاسم مطلوب"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "الاسم", "LABEL": "الاسم",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "الاسم مطلوب" "ERROR": "الاسم مطلوب"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Име", "LABEL": "Име",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Име", "LABEL": "Име",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Suprimeix"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Nom", "LABEL": "Nom",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Nom", "LABEL": "Nom",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Odebrat"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Název", "LABEL": "Název",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Název", "LABEL": "Název",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Søg emojis", "PLACEHOLDER": "Søg emojis",
"NOT_FOUND": "Ingen emoji matcher din søgning" "NOT_FOUND": "Ingen emoji matcher din søgning",
"REMOVE": "Fjern"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Navn", "LABEL": "Navn",
"PLACEHOLDER": "Kategori navn", "PLACEHOLDER": "Kategori navn",
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Navn er påkrævet" "ERROR": "Navn er påkrævet"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Navn", "LABEL": "Navn",
"PLACEHOLDER": "Kategori navn", "PLACEHOLDER": "Kategori navn",
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Navn er påkrævet" "ERROR": "Navn er påkrævet"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Emojis durchsuchen", "PLACEHOLDER": "Emojis durchsuchen",
"NOT_FOUND": "Kein Emoji entspricht deiner Suche" "NOT_FOUND": "Kein Emoji entspricht deiner Suche",
"REMOVE": "Entfernen"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Kategoriename", "PLACEHOLDER": "Kategoriename",
"HELP_TEXT": "Der Kategoriename wird im öffentlich zugänglichen Portal verwendet, um Artikel zu kategorisieren.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name wird benötigt" "ERROR": "Name wird benötigt"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Kategoriename", "PLACEHOLDER": "Kategoriename",
"HELP_TEXT": "Der Kategoriename wird im öffentlich zugänglichen Portal verwendet, um Artikel zu kategorisieren.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name wird benötigt" "ERROR": "Name wird benötigt"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Αναζήτηση emojis", "PLACEHOLDER": "Αναζήτηση emojis",
"NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας" "NOT_FOUND": "Κανένα emoji δεν ταιριάζει με την αναζήτησή σας",
"REMOVE": "Διαγραφή"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Όνομα", "LABEL": "Όνομα",
"PLACEHOLDER": "Όνομα κατηγορίας", "PLACEHOLDER": "Όνομα κατηγορίας",
"HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Απαιτείται όνομα" "ERROR": "Απαιτείται όνομα"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Όνομα", "LABEL": "Όνομα",
"PLACEHOLDER": "Όνομα κατηγορίας", "PLACEHOLDER": "Όνομα κατηγορίας",
"HELP_TEXT": "Η κατηγορία θα χρησιμοποιηθεί στην πύλη που βλέπει το κοινό για την κατηγοριοποίηση των άρθρων.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Απαιτείται όνομα" "ERROR": "Απαιτείται όνομα"
}, },
"SLUG": { "SLUG": {

View File

@ -1,18 +1,18 @@
{ {
"FILTER": { "FILTER": {
"TITLE": "Filter Conversations", "TITLE": "Filter conversations",
"SUBTITLE": "Add filters below and hit 'Apply filters' to filter conversations.", "SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.",
"EDIT_CUSTOM_FILTER": "Edit Folder", "EDIT_CUSTOM_FILTER": "Edit Folder",
"CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.", "CUSTOM_VIEWS_SUBTITLE": "Add or remove filters and update your folder.",
"ADD_NEW_FILTER": "Add Filter", "ADD_NEW_FILTER": "Add filter",
"FILTER_DELETE_ERROR": "You should have atleast one filter to save", "FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.",
"SUBMIT_BUTTON_LABEL": "Apply filters", "SUBMIT_BUTTON_LABEL": "Apply filters",
"UPDATE_BUTTON_LABEL": "Update folder", "UPDATE_BUTTON_LABEL": "Update folder",
"CANCEL_BUTTON_LABEL": "Cancel", "CANCEL_BUTTON_LABEL": "Cancel",
"CLEAR_BUTTON_LABEL": "Clear Filters", "CLEAR_BUTTON_LABEL": "Clear filters",
"EMPTY_VALUE_ERROR": "Value is required",
"FOLDER_LABEL": "Folder Name", "FOLDER_LABEL": "Folder Name",
"FOLDER_QUERY_LABEL": "Folder Query", "FOLDER_QUERY_LABEL": "Folder Query",
"EMPTY_VALUE_ERROR": "Value is required.",
"TOOLTIP_LABEL": "Filter conversations", "TOOLTIP_LABEL": "Filter conversations",
"QUERY_DROPDOWN_LABELS": { "QUERY_DROPDOWN_LABELS": {
"AND": "AND", "AND": "AND",
@ -36,44 +36,44 @@
}, },
"ATTRIBUTES": { "ATTRIBUTES": {
"STATUS": "Status", "STATUS": "Status",
"ASSIGNEE_NAME": "Assignee Name", "ASSIGNEE_NAME": "Assignee name",
"INBOX_NAME": "Inbox Name", "INBOX_NAME": "Inbox name",
"TEAM_NAME": "Team Name", "TEAM_NAME": "Team name",
"CONVERSATION_IDENTIFIER": "Conversation Identifier", "CONVERSATION_IDENTIFIER": "Conversation identifier",
"CAMPAIGN_NAME": "Campaign Name", "CAMPAIGN_NAME": "Campaign name",
"LABELS": "Labels", "LABELS": "Labels",
"BROWSER_LANGUAGE": "Browser Language", "BROWSER_LANGUAGE": "Browser language",
"PRIORITY": "Priority", "PRIORITY": "Priority",
"COUNTRY_NAME": "Country Name", "COUNTRY_NAME": "Country name",
"REFERER_LINK": "Referer link", "REFERER_LINK": "Referer link",
"CUSTOM_ATTRIBUTE_LIST": "List", "CUSTOM_ATTRIBUTE_LIST": "List",
"CUSTOM_ATTRIBUTE_TEXT": "Text", "CUSTOM_ATTRIBUTE_TEXT": "Text",
"CUSTOM_ATTRIBUTE_NUMBER": "Number", "CUSTOM_ATTRIBUTE_NUMBER": "Number",
"CUSTOM_ATTRIBUTE_LINK": "Link", "CUSTOM_ATTRIBUTE_LINK": "Link",
"CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox", "CUSTOM_ATTRIBUTE_CHECKBOX": "Checkbox",
"CREATED_AT": "Created At", "CREATED_AT": "Created at",
"LAST_ACTIVITY": "Last Activity" "LAST_ACTIVITY": "Last activity"
}, },
"GROUPS": { "GROUPS": {
"STANDARD_FILTERS": "Standard Filters", "STANDARD_FILTERS": "Standard filters",
"ADDITIONAL_FILTERS": "Additional Filters", "ADDITIONAL_FILTERS": "Additional filters",
"CUSTOM_ATTRIBUTES": "Custom Attributes" "CUSTOM_ATTRIBUTES": "Custom attributes"
}, },
"CUSTOM_VIEWS": { "CUSTOM_VIEWS": {
"ADD": { "ADD": {
"TITLE": "Do you want to save this filter?", "TITLE": "Do you want to save this filter?",
"LABEL": "Name this filter", "LABEL": "Name this filter",
"PLACEHOLDER": "Enter a name for this filter", "PLACEHOLDER": "Name your filter to refer it later.",
"ERROR_MESSAGE": "Name is required", "ERROR_MESSAGE": "Name is required.",
"SAVE_BUTTON": "Save filter", "SAVE_BUTTON": "Save filter",
"CANCEL_BUTTON": "Cancel", "CANCEL_BUTTON": "Cancel",
"API_FOLDERS": { "API_FOLDERS": {
"SUCCESS_MESSAGE": "Folder created successfully", "SUCCESS_MESSAGE": "Folder created successfully.",
"ERROR_MESSAGE": "Error while creating folder" "ERROR_MESSAGE": "Error while creating folder."
}, },
"API_SEGMENTS": { "API_SEGMENTS": {
"SUCCESS_MESSAGE": "Segment created successfully", "SUCCESS_MESSAGE": "Segment created successfully.",
"ERROR_MESSAGE": "Error while creating segment" "ERROR_MESSAGE": "Error while creating segment."
} }
}, },
"EDIT": { "EDIT": {
@ -83,19 +83,19 @@
"DELETE_BUTTON": "Delete filter", "DELETE_BUTTON": "Delete filter",
"MODAL": { "MODAL": {
"CONFIRM": { "CONFIRM": {
"TITLE": "Confirm Deletion", "TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete the filter ", "MESSAGE": "Are you sure to delete the filter ",
"YES": "Yes, Delete", "YES": "Yes, delete",
"NO": "No, Keep it" "NO": "No, keep it"
} }
}, },
"API_FOLDERS": { "API_FOLDERS": {
"SUCCESS_MESSAGE": "Folder deleted successfully", "SUCCESS_MESSAGE": "Folder deleted successfully.",
"ERROR_MESSAGE": "Error while deleting folder" "ERROR_MESSAGE": "Error while deleting folder."
}, },
"API_SEGMENTS": { "API_SEGMENTS": {
"SUCCESS_MESSAGE": "Segment deleted successfully", "SUCCESS_MESSAGE": "Segment deleted successfully.",
"ERROR_MESSAGE": "Error while deleting segment" "ERROR_MESSAGE": "Error while deleting segment."
} }
} }
} }

View File

@ -1,73 +1,73 @@
{ {
"AGENT_BOTS": { "AGENT_BOTS": {
"HEADER": "Bots", "HEADER": "Bots",
"LOADING_EDITOR": "Loading Editor...", "LOADING_EDITOR": "Loading editor...",
"HEADER_BTN_TXT": "Add Bot Configuration", "HEADER_BTN_TXT": "Add bot configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>", "SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent Bots are like the most fabulous members of your team. They can handle the small stuff, so you can focus on the stuff that matters. Give them a try.</p> <p> You can manage your bots from this page or create new ones using the 'Add bot configuraton' button.</p> <p> Open the <a href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677497472-how-to-use-agent-bots\" target=\"blank\">Agent bots handbook</a> in another tab for a helping hand.</p>",
"CSML_BOT_EDITOR": { "CSML_BOT_EDITOR": {
"NAME": { "NAME": {
"LABEL": "Bot Name", "LABEL": "Bot name",
"PLACEHOLDER": "Give your bot a name", "PLACEHOLDER": "Name your bot.",
"ERROR": "Bot name is required" "ERROR": "Bot name is required."
}, },
"DESCRIPTION": { "DESCRIPTION": {
"LABEL": "Bot Description", "LABEL": "Bot description",
"PLACEHOLDER": "What does this bot do?" "PLACEHOLDER": "What does this bot do?"
}, },
"BOT_CONFIG": { "BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above", "ERROR": "Please enter your CSML bot configuration above.",
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again." "API_ERROR": "Your CSML configuration is invalid. Please fix it and try again."
}, },
"SUBMIT": "Validate and save" "SUBMIT": "Validate and save"
}, },
"BOT_CONFIGURATION": { "BOT_CONFIGURATION": {
"TITLE": "Select an agent bot", "TITLE": "Select an agent bot",
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.", "DESC": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.",
"SUBMIT": "Update", "SUBMIT": "Update",
"DISCONNECT": "Disconnect Bot", "DISCONNECT": "Disconnect bot",
"SUCCESS_MESSAGE": "Successfully updated the agent bot", "SUCCESS_MESSAGE": "Successfully updated the agent bot.",
"DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot", "DISCONNECTED_SUCCESS_MESSAGE": "Successfully disconnected the agent bot.",
"ERROR_MESSAGE": "Could not update the agent bot, please try again later", "ERROR_MESSAGE": "Could not update the agent bot. Please try again.",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later", "DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.",
"SELECT_PLACEHOLDER": "Select Bot" "SELECT_PLACEHOLDER": "Select bot"
}, },
"ADD": { "ADD": {
"TITLE": "Configure new bot", "TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"API": { "API": {
"SUCCESS_MESSAGE": "Bot added successfully", "SUCCESS_MESSAGE": "Bot added successfully.",
"ERROR_MESSAGE": "Could not add bot, Please try again later" "ERROR_MESSAGE": "Could not add bot. Please try again later."
} }
}, },
"LIST": { "LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗", "404": "No bots found. You can create a bot by clicking the 'Configure new bot' button ↗",
"LOADING": "Fetching Bots...", "LOADING": "Fetching bots...",
"TYPE": "Bot Type" "TYPE": "Bot type"
}, },
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Delete", "BUTTON_TEXT": "Delete",
"TITLE": "Delete Bot", "TITLE": "Delete bot",
"SUBMIT": "Delete", "SUBMIT": "Delete",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible", "DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible.",
"API": { "API": {
"SUCCESS_MESSAGE": "Bot deleted successfully", "SUCCESS_MESSAGE": "Bot deleted successfully.",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later" "ERROR_MESSAGE": "Could not delete bot. Please try again."
} }
}, },
"EDIT": { "EDIT": {
"BUTTON_TEXT": "Edit", "BUTTON_TEXT": "Edit",
"LOADING": "Fetching Bots...", "LOADING": "Fetching bots...",
"TITLE": "Edit Bot", "TITLE": "Edit bot",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"API": { "API": {
"SUCCESS_MESSAGE": "Bot updated successfully", "SUCCESS_MESSAGE": "Bot updated successfully.",
"ERROR_MESSAGE": "Could not update bot, Please try again later" "ERROR_MESSAGE": "Could not update bot. Please try again."
} }
}, },
"TYPES": { "TYPES": {
"WEBHOOK": "Webhook Bot", "WEBHOOK": "Webhook bot",
"CSML": "CSML Bot" "CSML": "CSML bot"
} }
} }
} }

View File

@ -1,40 +1,40 @@
{ {
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Select Agent", "AGENT_SELECT_LABEL": "Select agent",
"ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to", "ASSIGN_CONFIRMATION_LABEL": "Are you sure to assign %{conversationCount} %{conversationLabel} to",
"UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?", "UNASSIGN_CONFIRMATION_LABEL": "Are you sure to unassign %{conversationCount} %{conversationLabel}?",
"GO_BACK_LABEL": "Go back", "GO_BACK_LABEL": "Go back",
"ASSIGN_LABEL": "Assign", "ASSIGN_LABEL": "Assign",
"YES": "Yes", "YES": "Yes",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign agent",
"ASSIGN_TEAM_TOOLTIP": "Assign team", "ASSIGN_TEAM_TOOLTIP": "Assign team",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_SUCCESFUL": "Conversations assigned successfully.",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "ASSIGN_FAILED": "Failed to assign conversations. Please try again.",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully", "RESOLVE_SUCCESFUL": "Conversations resolved successfully.",
"RESOLVE_FAILED": "Failed to resolve conversations, please try again", "RESOLVE_FAILED": "Failed to resolve conversations. Please try again.",
"ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.", "ALL_CONVERSATIONS_SELECTED_ALERT": "Conversations visible on this page are only selected.",
"AGENT_LIST_LOADING": "Loading Agents", "AGENT_LIST_LOADING": "Loading agents",
"UPDATE": { "UPDATE": {
"CHANGE_STATUS": "Change status", "CHANGE_STATUS": "Change status",
"SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply", "SNOOZE_UNTIL_NEXT_REPLY": "Snooze until next reply.",
"UPDATE_SUCCESFUL": "Conversation status updated successfully.", "UPDATE_SUCCESFUL": "Conversation status updated successfully.",
"UPDATE_FAILED": "Failed to update conversations, please try again" "UPDATE_FAILED": "Failed to update conversations. Please try again."
}, },
"LABELS": { "LABELS": {
"ASSIGN_LABELS": "Assign Labels", "ASSIGN_LABELS": "Assign labels",
"NO_LABELS_FOUND": "No labels found for", "NO_LABELS_FOUND": "No labels found for",
"ASSIGN_SELECTED_LABELS": "Assign selected labels", "ASSIGN_SELECTED_LABELS": "Assign selected labels",
"ASSIGN_SUCCESFUL": "Labels assigned successfully", "ASSIGN_SUCCESFUL": "Labels assigned successfully.",
"ASSIGN_FAILED": "Failed to assign labels, please try again" "ASSIGN_FAILED": "Failed to assign labels. Please try again."
}, },
"TEAMS": { "TEAMS": {
"TEAM_SELECT_LABEL": "Select Team", "TEAM_SELECT_LABEL": "Select team",
"NONE": "None", "NONE": "None",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.", "NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team", "ASSIGN_SELECTED_TEAMS": "Assign selected team.",
"ASSIGN_SUCCESFUL": "Teams assiged successfully", "ASSIGN_SUCCESFUL": "Teams assiged successfully.",
"ASSIGN_FAILED": "Failed to assign team, please try again" "ASSIGN_FAILED": "Failed to assign team. Please try again."
} }
} }
} }

View File

@ -1,71 +1,71 @@
{ {
"CANNED_MGMT": { "CANNED_MGMT": {
"HEADER": "Canned Responses", "HEADER": "Canned Responses",
"HEADER_BTN_TXT": "Add Canned Response", "HEADER_BTN_TXT": "Add canned response",
"LOADING": "Fetching Canned Responses", "LOADING": "Fetching canned responses...",
"SEARCH_404": "There are no items matching this query", "SEARCH_404": "There are no items matching this query.",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>", "SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
"LIST": { "LIST": {
"404": "There are no canned responses available in this account.", "404": "There are no canned responses available in this account.",
"TITLE": "Manage canned responses", "TITLE": "Manage canned responses",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to tickets.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"TABLE_HEADER": ["Short Code", "Content", "Actions"] "TABLE_HEADER": ["Short code", "Content", "Actions"]
}, },
"ADD": { "ADD": {
"TITLE": "Add Canned Response", "TITLE": "Add canned response",
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Short Code", "LABEL": "Short code",
"PLACEHOLDER": "Please enter a short code", "PLACEHOLDER": "Please enter a short code.",
"ERROR": "Short Code is required" "ERROR": "Short Code is required."
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Content", "LABEL": "Message",
"PLACEHOLDER": "Please enter a content", "PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Content is required" "ERROR": "Message is required."
}, },
"SUBMIT": "Submit" "SUBMIT": "Submit"
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Canned Response added successfully", "SUCCESS_MESSAGE": "Canned response added successfully.",
"ERROR_MESSAGE": "Could not create canned response. Please try again later." "ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
} }
}, },
"EDIT": { "EDIT": {
"TITLE": "Edit Canned Response", "TITLE": "Edit canned response",
"CANCEL_BUTTON_TEXT": "Cancel", "CANCEL_BUTTON_TEXT": "Cancel",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Short Code", "LABEL": "Short code",
"PLACEHOLDER": "Please enter a shortcode", "PLACEHOLDER": "Please enter a shortcode.",
"ERROR": "Short Code is required" "ERROR": "Short code is required."
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Content", "LABEL": "Message",
"PLACEHOLDER": "Please enter a content", "PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Content is required" "ERROR": "Message is required."
}, },
"SUBMIT": "Submit" "SUBMIT": "Submit"
}, },
"BUTTON_TEXT": "Edit", "BUTTON_TEXT": "Edit",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned Response updated successfully", "SUCCESS_MESSAGE": "Canned response is updated successfully.",
"ERROR_MESSAGE": "Could not update canned response. Please try again later." "ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
} }
}, },
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Delete", "BUTTON_TEXT": "Delete",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response deleted successfully", "SUCCESS_MESSAGE": "Canned response deleted successfully.",
"ERROR_MESSAGE": "Could not delete canned response. Please try again later." "ERROR_MESSAGE": "Could not connect to Woot server. Please try again."
}, },
"CONFIRM": { "CONFIRM": {
"TITLE": "Confirm Deletion", "TITLE": "Confirm deletion",
"MESSAGE": "Are you sure to delete ", "MESSAGE": "Are you sure to delete ",
"YES": "Yes, Delete ", "YES": "Yes, delete ",
"NO": "No, Keep " "NO": "No, keep "
} }
} }
} }

View File

@ -139,6 +139,7 @@
"PRIVATE_NOTE": "Private Note", "PRIVATE_NOTE": "Private Note",
"SEND": "Send", "SEND": "Send",
"CREATE": "Add Note", "CREATE": "Add Note",
"INSERT_READ_MORE": "Read more",
"DISMISS_REPLY": "Dismiss reply", "DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:", "REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Show rich text editor", "TIP_FORMAT_ICON": "Show rich text editor",

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
} }
} }

View File

@ -76,6 +76,9 @@
}, },
"ARTICLE_SEARCH_RESULT": { "ARTICLE_SEARCH_RESULT": {
"UNCATEGORIZED": "Uncategorized", "UNCATEGORIZED": "Uncategorized",
"SEARCH_RESULTS": "Search results for %{query}",
"EMPTY_TEXT": "Search for articles to insert into replies.",
"SEARCH_LOADER": "Searching...",
"INSERT_ARTICLE": "Insert", "INSERT_ARTICLE": "Insert",
"NO_RESULT": "No articles found", "NO_RESULT": "No articles found",
"COPY_LINK": "Copy article link to clipboard", "COPY_LINK": "Copy article link to clipboard",
@ -220,7 +223,10 @@
"LOGO": { "LOGO": {
"LABEL": "Logo", "LABEL": "Logo",
"UPLOAD_BUTTON": "Upload logo", "UPLOAD_BUTTON": "Upload logo",
"HELP_TEXT": "This logo will be displayed on the portal header." "HELP_TEXT": "This logo will be displayed on the portal header.",
"IMAGE_UPLOAD_SUCCESS": "Logo uploaded successfully",
"IMAGE_UPLOAD_ERROR": "Logo deleted successfully",
"IMAGE_DELETE_ERROR": "Error while deleting logo"
}, },
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
@ -365,7 +371,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@ -396,7 +402,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@ -426,6 +432,21 @@
} }
} }
}, },
"ARTICLE_SEARCH": {
"TITLE": "Search articles",
"PLACEHOLDER": "Search articles",
"NO_RESULT": "No articles found",
"SEARCHING": "Searching...",
"SEARCH_BUTTON": "Search",
"INSERT_ARTICLE": "Insert link",
"IFRAME_ERROR": "URL is empty or invalid. Unable to display content.",
"OPEN_ARTICLE_SEARCH": "Insert article from Help Center",
"SUCCESS_ARTICLE_INSERTED": "Article inserted successfully",
"PREVIEW_LINK": "Preview article",
"CANCEL": "Close",
"BACK": "Back",
"BACK_RESULTS": "Back to results"
},
"UPGRADE_PAGE": { "UPGRADE_PAGE": {
"TITLE": "Help Center", "TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.", "DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",

View File

@ -139,8 +139,8 @@
"PRIVATE_NOTE": "Nota privada", "PRIVATE_NOTE": "Nota privada",
"SEND": "Enviar", "SEND": "Enviar",
"CREATE": "Añadir nota", "CREATE": "Añadir nota",
"DISMISS_REPLY": "Dismiss reply", "DISMISS_REPLY": "Descartar respuesta",
"REPLYING_TO": "Replying to:", "REPLYING_TO": "Respondiendo a:",
"TIP_FORMAT_ICON": "Mostrar editor de textos", "TIP_FORMAT_ICON": "Mostrar editor de textos",
"TIP_EMOJI_ICON": "Mostrar selector de emoji", "TIP_EMOJI_ICON": "Mostrar selector de emoji",
"TIP_ATTACH_ICON": "Adjuntar archivos", "TIP_ATTACH_ICON": "Adjuntar archivos",
@ -195,7 +195,7 @@
}, },
"CONTEXT_MENU": { "CONTEXT_MENU": {
"COPY": "Copiar", "COPY": "Copiar",
"REPLY_TO": "Reply to this message", "REPLY_TO": "Responder a este mensaje",
"DELETE": "Eliminar", "DELETE": "Eliminar",
"CREATE_A_CANNED_RESPONSE": "Añadir a respuestas predefinidas", "CREATE_A_CANNED_RESPONSE": "Añadir a respuestas predefinidas",
"TRANSLATE": "Traducir", "TRANSLATE": "Traducir",

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Buscar emojis", "PLACEHOLDER": "Buscar emojis",
"NOT_FOUND": "Ningún emoji coincide con tu búsqueda" "NOT_FOUND": "Ningún emoji coincide con tu búsqueda",
"REMOVE": "Eliminar"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Nombre", "LABEL": "Nombre",
"PLACEHOLDER": "Nombre de categoría", "PLACEHOLDER": "Nombre de categoría",
"HELP_TEXT": "El nombre de la categoría se utilizará en el portal público orientado a categorizar los artículos.", "HELP_TEXT": "El nombre de la categoría y el icono se utilizarán en el portal público para categorizar los artículos.",
"ERROR": "El nombre es requerido" "ERROR": "El nombre es requerido"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Nombre", "LABEL": "Nombre",
"PLACEHOLDER": "Nombre de categoría", "PLACEHOLDER": "Nombre de categoría",
"HELP_TEXT": "El nombre de la categoría se utilizará en el portal público orientado a categorizar los artículos.", "HELP_TEXT": "El nombre de la categoría y el icono se utilizarán en el portal público para categorizar los artículos.",
"ERROR": "El nombre es requerido" "ERROR": "El nombre es requerido"
}, },
"SLUG": { "SLUG": {
@ -428,28 +428,28 @@
}, },
"UPGRADE_PAGE": { "UPGRADE_PAGE": {
"TITLE": "Centro de ayuda", "TITLE": "Centro de ayuda",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.", "DESCRIPTION": "Crear portales de autoservicio amigables. Ayuda a tus usuarios a acceder a los artículos y obtener soporte 24/7. Actualiza tu suscripción para activar esta característica.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.", "SELF_HOSTED_DESCRIPTION": "Crear portales de autoservicio amigables. Ayuda a tus usuarios a acceder a los artículos y obtener soporte 24/7. Póngase en contacto con su administrador para habilitar esta característica.",
"BUTTON": { "BUTTON": {
"LEARN_MORE": "Más información", "LEARN_MORE": "Más información",
"UPGRADE": "Upgrade" "UPGRADE": "Mejorar"
}, },
"FEATURES": { "FEATURES": {
"PORTALS": { "PORTALS": {
"TITLE": "Multiple portals", "TITLE": "Múltiples portales",
"DESCRIPTION": "Create multiple help center portals for different products using the same account." "DESCRIPTION": "Crear múltiples portales de centro de ayuda para diferentes productos usando la misma cuenta."
}, },
"LOCALES": { "LOCALES": {
"TITLE": "Full support for locales", "TITLE": "Soporte completo para locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article." "DESCRIPTION": "Localiza el portal en tu idioma. Apoyamos todas las localizaciones y permitimos traducciones para cada artículo."
}, },
"SEO": { "SEO": {
"TITLE": "SEO-friendly design", "TITLE": "Diseño amigable con SEO",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages." "DESCRIPTION": "Personalice sus meta-etiquetas para mejorar su visibilidad en los motores de búsqueda con nuestras páginas amigables con SEO."
}, },
"API": { "API": {
"TITLE": "Full API support", "TITLE": "Soporte completo de API",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs." "DESCRIPTION": "Utilice el portal como un CMS sin cabeceras con frameworks front-end de terceros usando nuestras APIs."
} }
} }
} }

View File

@ -39,7 +39,7 @@
"SORT_TOOLTIP_LABEL": "مرتب‌سازی گفتگوها", "SORT_TOOLTIP_LABEL": "مرتب‌سازی گفتگوها",
"CHAT_SORT": { "CHAT_SORT": {
"STATUS": "وضعیت", "STATUS": "وضعیت",
"ORDER_BY": "سفارش توسط" "ORDER_BY": "مرتب بر اساس"
}, },
"CHAT_TIME_STAMP": { "CHAT_TIME_STAMP": {
"CREATED": { "CREATED": {
@ -62,7 +62,7 @@
"TEXT": "اولویت" "TEXT": "اولویت"
}, },
"sort_on_waiting_since": { "sort_on_waiting_since": {
"TEXT": "پاسخ در انتظار" "TEXT": "در انتظار پاسخ"
} }
}, },
"ATTACHMENTS": { "ATTACHMENTS": {

View File

@ -9,7 +9,7 @@
"LOCATION": "مکان", "LOCATION": "مکان",
"BROWSER_LANGUAGE": "مرور زبان", "BROWSER_LANGUAGE": "مرور زبان",
"CONVERSATION_TITLE": "جزئیات مکالمه", "CONVERSATION_TITLE": "جزئیات مکالمه",
"VIEW_PROFILE": "نمایش مشخصات", "VIEW_PROFILE": "نمایش پروفایل",
"BROWSER": "مرورگر", "BROWSER": "مرورگر",
"OS": "سیستم عامل", "OS": "سیستم عامل",
"INITIATED_FROM": "شروع شده از", "INITIATED_FROM": "شروع شده از",
@ -62,13 +62,13 @@
"DESC": "اطلاعات اولیه درباره مخاطب را اضافه کنید." "DESC": "اطلاعات اولیه درباره مخاطب را اضافه کنید."
}, },
"IMPORT_CONTACTS": { "IMPORT_CONTACTS": {
"BUTTON_LABEL": "وارد كردن", "BUTTON_LABEL": "تزریق کردن",
"TITLE": "وارد کردن مخاطبین", "TITLE": "تزریق مخاطبین",
"DESC": "مخاطبین را از طریق یک فایل CSV وارد کنید.", "DESC": "مخاطبین را از طریق یک فایل CSV وارد کنید.",
"DOWNLOAD_LABEL": "نمونه csv را بارگیری کنید.", "DOWNLOAD_LABEL": "نمونه csv را بارگیری کنید.",
"FORM": { "FORM": {
"LABEL": "پرونده CSV", "LABEL": "پرونده CSV",
"SUBMIT": "وارد كردن", "SUBMIT": "تزریق کردن",
"CANCEL": "انصراف" "CANCEL": "انصراف"
}, },
"SUCCESS_MESSAGE": "You will be notified via email when the import is complete.", "SUCCESS_MESSAGE": "You will be notified via email when the import is complete.",

View File

@ -1,7 +1,7 @@
{ {
"CONVERSATION": { "CONVERSATION": {
"SELECT_A_CONVERSATION": "لطفا یک گفتگو را از پنجره گفتگوها انتخاب کنید", "SELECT_A_CONVERSATION": "لطفا یک گفتگو را از پنجره گفتگوها انتخاب کنید",
"CSAT_REPLY_MESSAGE": "لطفاً به مکالمه امتیاز دهید", "CSAT_REPLY_MESSAGE": "لطفاً به گفتگو امتیاز دهید",
"404": "با عرض پوزش، ما نمی‌توانیم گفتگو را پیدا کنیم. لطفا دوباره تلاش کنید", "404": "با عرض پوزش، ما نمی‌توانیم گفتگو را پیدا کنیم. لطفا دوباره تلاش کنید",
"SWITCH_VIEW_LAYOUT": "تغییر طرح‌بندی", "SWITCH_VIEW_LAYOUT": "تغییر طرح‌بندی",
"DASHBOARD_APP_TAB_MESSAGES": "پیام‌ها", "DASHBOARD_APP_TAB_MESSAGES": "پیام‌ها",
@ -37,7 +37,7 @@
"REPLYING_TO": "شما در حال پاسخ دادن به:", "REPLYING_TO": "شما در حال پاسخ دادن به:",
"REMOVE_SELECTION": "حذف انتخاب‌شده‌ها", "REMOVE_SELECTION": "حذف انتخاب‌شده‌ها",
"DOWNLOAD": "دانلود", "DOWNLOAD": "دانلود",
"UNKNOWN_FILE_TYPE": "پرونده ناشناخته", "UNKNOWN_FILE_TYPE": "فایل ناشناخته",
"SAVE_CONTACT": "ذخیره", "SAVE_CONTACT": "ذخیره",
"UPLOADING_ATTACHMENTS": "در حال بارگذاری پیوست‌ها...", "UPLOADING_ATTACHMENTS": "در حال بارگذاری پیوست‌ها...",
"REPLIED_TO_STORY": "Replied to your story", "REPLIED_TO_STORY": "Replied to your story",
@ -46,7 +46,7 @@
"NO_RESPONSE": "بدون پاسخ", "NO_RESPONSE": "بدون پاسخ",
"RATING_TITLE": "رتبه", "RATING_TITLE": "رتبه",
"FEEDBACK_TITLE": "بازخورد", "FEEDBACK_TITLE": "بازخورد",
"REPLY_MESSAGE_NOT_FOUND": "Message not available", "REPLY_MESSAGE_NOT_FOUND": "پیام در دسترس نیست",
"CARD": { "CARD": {
"SHOW_LABELS": "مشاهده کردن برچسب‌ها", "SHOW_LABELS": "مشاهده کردن برچسب‌ها",
"HIDE_LABELS": "پنهان کردن برچسب‌ها" "HIDE_LABELS": "پنهان کردن برچسب‌ها"
@ -58,7 +58,7 @@
"OPEN": "بیشتر", "OPEN": "بیشتر",
"CLOSE": "بستن", "CLOSE": "بستن",
"DETAILS": "جزئیات", "DETAILS": "جزئیات",
"SNOOZED_UNTIL": "Snoozed until", "SNOOZED_UNTIL": "به تعویق افتاده تا",
"SNOOZED_UNTIL_TOMORROW": "تا فردا به تعویق افتاد", "SNOOZED_UNTIL_TOMORROW": "تا فردا به تعویق افتاد",
"SNOOZED_UNTIL_NEXT_WEEK": "تا هفته آینده به تعویق افتاد", "SNOOZED_UNTIL_NEXT_WEEK": "تا هفته آینده به تعویق افتاد",
"SNOOZED_UNTIL_NEXT_REPLY": "تا پاسخ بعدی به تعویق افتاد" "SNOOZED_UNTIL_NEXT_REPLY": "تا پاسخ بعدی به تعویق افتاد"
@ -76,23 +76,23 @@
"CUSTOM_SNOOZE": { "CUSTOM_SNOOZE": {
"TITLE": "به تعویق انداختن تا", "TITLE": "به تعویق انداختن تا",
"APPLY": "به تعویق انداختن", "APPLY": "به تعویق انداختن",
"CANCEL": "انصراف" "CANCEL": "لغو"
}, },
"PRIORITY": { "PRIORITY": {
"TITLE": "اولویت", "TITLE": "اولویت",
"OPTIONS": { "OPTIONS": {
"NONE": "هیچکدام", "NONE": "هیچکدام",
"URGENT": "Urgent", "URGENT": "Urgent",
"HIGH": "High", "HIGH": "بالا",
"MEDIUM": "Medium", "MEDIUM": "متوسط",
"LOW": "Low" "LOW": "پایین"
}, },
"CHANGE_PRIORITY": { "CHANGE_PRIORITY": {
"SELECT_PLACEHOLDER": "هیچکدام", "SELECT_PLACEHOLDER": "هیچکدام",
"INPUT_PLACEHOLDER": "Select priority", "INPUT_PLACEHOLDER": "انتخاب اولویت",
"NO_RESULTS": "نتیجه‌ای یافت نشد", "NO_RESULTS": "نتیجه‌ای یافت نشد",
"SUCCESSFUL": "Changed priority of conversation id %{conversationId} to %{priority}", "SUCCESSFUL": "تغییر اولویت گفتگوی با شناسه %{conversationId} به %{priority}",
"FAILED": "Couldn't change priority. Please try again." "FAILED": "تغییر اولویت گفتگو با موفقیت انجام نشد. لطفا دوباره تلاش نمایید."
} }
}, },
"CARD_CONTEXT_MENU": { "CARD_CONTEXT_MENU": {
@ -139,8 +139,8 @@
"PRIVATE_NOTE": "یادداشت خصوصی", "PRIVATE_NOTE": "یادداشت خصوصی",
"SEND": "ارسال", "SEND": "ارسال",
"CREATE": "افزودن یادداشت", "CREATE": "افزودن یادداشت",
"DISMISS_REPLY": "Dismiss reply", "DISMISS_REPLY": "رد پاسخ",
"REPLYING_TO": "Replying to:", "REPLYING_TO": "در حال پاسخ دهی به:",
"TIP_FORMAT_ICON": "نمایش ویرایشگر متنی پیشرفته", "TIP_FORMAT_ICON": "نمایش ویرایشگر متنی پیشرفته",
"TIP_EMOJI_ICON": "انتخاب ایموجی", "TIP_EMOJI_ICON": "انتخاب ایموجی",
"TIP_ATTACH_ICON": "ضمیمه فایل", "TIP_ATTACH_ICON": "ضمیمه فایل",
@ -152,10 +152,10 @@
"STOP_AUDIO_RECORDING": "در حال توقف ضبط صدا", "STOP_AUDIO_RECORDING": "در حال توقف ضبط صدا",
"": "", "": "",
"EMAIL_HEAD": { "EMAIL_HEAD": {
"TO": "TO", "TO": "به",
"ADD_BCC": "افزودن bcc", "ADD_BCC": "افزودن رونوشت",
"CC": { "CC": {
"LABEL": "CC", "LABEL": "رونوشت",
"PLACEHOLDER": "ایمیل‌ها با کاما از هم جدا می‌شوند", "PLACEHOLDER": "ایمیل‌ها با کاما از هم جدا می‌شوند",
"ERROR": "لطفا آدرس‌ ایمیل معتبر وارد کنید" "ERROR": "لطفا آدرس‌ ایمیل معتبر وارد کنید"
}, },
@ -195,7 +195,7 @@
}, },
"CONTEXT_MENU": { "CONTEXT_MENU": {
"COPY": "کپی", "COPY": "کپی",
"REPLY_TO": "Reply to this message", "REPLY_TO": "پاسخ به این پیام",
"DELETE": "حذف", "DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "اضافه کردن به پاسخ‌های آماده", "CREATE_A_CANNED_RESPONSE": "اضافه کردن به پاسخ‌های آماده",
"TRANSLATE": "ترجمه", "TRANSLATE": "ترجمه",
@ -296,7 +296,7 @@
"SUBJECT": "موضوع" "SUBJECT": "موضوع"
}, },
"CONVERSATION_PARTICIPANTS": { "CONVERSATION_PARTICIPANTS": {
"SIDEBAR_MENU_TITLE": "شرکت کننده", "SIDEBAR_MENU_TITLE": "شرکت کنندگان",
"SIDEBAR_TITLE": "شرکت کنندگان در گفتگو", "SIDEBAR_TITLE": "شرکت کنندگان در گفتگو",
"NO_RECORDS_FOUND": "نتیجه‌ای یافت نشد", "NO_RECORDS_FOUND": "نتیجه‌ای یافت نشد",
"ADD_PARTICIPANTS": "شرکت کنندگان را انتخاب کنید", "ADD_PARTICIPANTS": "شرکت کنندگان را انتخاب کنید",

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "جستجوی ایموجی", "PLACEHOLDER": "جستجوی ایموجی",
"NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد" "NOT_FOUND": "هیچ ایموجی با جستجوی شما مطابقت ندارد",
"REMOVE": "حذف"
} }
} }

View File

@ -3,7 +3,7 @@
"TITLE": "تنظیمات حساب", "TITLE": "تنظیمات حساب",
"SUBMIT": "به‌روزرسانی تنظیمات", "SUBMIT": "به‌روزرسانی تنظیمات",
"BACK": "بازگشت", "BACK": "بازگشت",
"DISMISS": "Dismiss", "DISMISS": "نادیده بگیر",
"UPDATE": { "UPDATE": {
"ERROR": "تنظیمات به‌روزرسانی نشد، دوباره امتحان کنید!", "ERROR": "تنظیمات به‌روزرسانی نشد، دوباره امتحان کنید!",
"SUCCESS": "تنظیمات با موفقیت اعمال شد" "SUCCESS": "تنظیمات با موفقیت اعمال شد"
@ -92,7 +92,7 @@
}, },
"NETWORK": { "NETWORK": {
"NOTIFICATION": { "NOTIFICATION": {
"TEXT": "قطع کردن Chatwoot" "TEXT": "ارتباط با سیستم چت قطع شده است"
}, },
"BUTTON": { "BUTTON": {
"REFRESH": "تازه کردن" "REFRESH": "تازه کردن"
@ -112,7 +112,7 @@
"REMOVE_LABEL": "برچسب را از مکالمه حذف کنید", "REMOVE_LABEL": "برچسب را از مکالمه حذف کنید",
"SETTINGS": "تنظیمات", "SETTINGS": "تنظیمات",
"AI_ASSIST": "AI Assist", "AI_ASSIST": "AI Assist",
"APPEARANCE": "Appearance" "APPEARANCE": "ظاهری"
}, },
"COMMANDS": { "COMMANDS": {
"GO_TO_CONVERSATION_DASHBOARD": "به داشبورد مکالمه بروید", "GO_TO_CONVERSATION_DASHBOARD": "به داشبورد مکالمه بروید",
@ -135,7 +135,7 @@
"ADD_LABELS_TO_CONVERSATION": "برچسب را به مکالمه اضافه کنید", "ADD_LABELS_TO_CONVERSATION": "برچسب را به مکالمه اضافه کنید",
"ASSIGN_AN_AGENT": "یک ایجنت تعیین کنید", "ASSIGN_AN_AGENT": "یک ایجنت تعیین کنید",
"AI_ASSIST": "AI Assist", "AI_ASSIST": "AI Assist",
"ASSIGN_PRIORITY": "Assign priority", "ASSIGN_PRIORITY": "انتساب اولویت",
"ASSIGN_A_TEAM": "یک تیم را تعیین کنید", "ASSIGN_A_TEAM": "یک تیم را تعیین کنید",
"MUTE_CONVERSATION": "صدای گفتگو را قطع کن", "MUTE_CONVERSATION": "صدای گفتگو را قطع کن",
"UNMUTE_CONVERSATION": "مکالمه را باصدا کنید", "UNMUTE_CONVERSATION": "مکالمه را باصدا کنید",
@ -147,8 +147,8 @@
"UNTIL_NEXT_REPLY": "تا پاسخ بعدی", "UNTIL_NEXT_REPLY": "تا پاسخ بعدی",
"UNTIL_NEXT_WEEK": "تا هفته بعد", "UNTIL_NEXT_WEEK": "تا هفته بعد",
"UNTIL_TOMORROW": "تا فردا", "UNTIL_TOMORROW": "تا فردا",
"UNTIL_NEXT_MONTH": "Until next month", "UNTIL_NEXT_MONTH": "تا ماه آینده",
"AN_HOUR_FROM_NOW": "Until an hour from now", "AN_HOUR_FROM_NOW": "از حالا تا یک ساعت دیگر",
"CUSTOM": "سفارشی...", "CUSTOM": "سفارشی...",
"CHANGE_APPEARANCE": "تغییر ظاهر", "CHANGE_APPEARANCE": "تغییر ظاهر",
"LIGHT_MODE": "روشن", "LIGHT_MODE": "روشن",

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "نام", "LABEL": "نام",
"PLACEHOLDER": "نام دسته‌بندی", "PLACEHOLDER": "نام دسته‌بندی",
"HELP_TEXT": "از نام دسته در پرتال عمومی برای دسته بندی مقالات استفاده می شود.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "نام الزامی است" "ERROR": "نام الزامی است"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "نام", "LABEL": "نام",
"PLACEHOLDER": "نام دسته‌بندی", "PLACEHOLDER": "نام دسته‌بندی",
"HELP_TEXT": "از نام دسته در پرتال عمومی برای دسته بندی مقالات استفاده می شود.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "نام الزامی است" "ERROR": "نام الزامی است"
}, },
"SLUG": { "SLUG": {

View File

@ -162,7 +162,7 @@
"PROVIDERS": { "PROVIDERS": {
"LABEL": "ارائه دهنده API", "LABEL": "ارائه دهنده API",
"TWILIO": "Twilio", "TWILIO": "Twilio",
"BANDWIDTH": "Bandwidth" "BANDWIDTH": "پهنای باند"
}, },
"API": { "API": {
"ERROR_MESSAGE": "ما نتوانستیم کانال پیامک را ذخیره کنیم" "ERROR_MESSAGE": "ما نتوانستیم کانال پیامک را ذخیره کنیم"
@ -405,21 +405,21 @@
"DISABLED": "غیرفعال" "DISABLED": "غیرفعال"
}, },
"SENDER_NAME_SECTION": { "SENDER_NAME_SECTION": {
"TITLE": "Sender name", "TITLE": "نام ارسال کننده",
"SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.", "SUB_TEXT": "Select the name shown to the your customer when they receive emails from your agents.",
"FOR_EG": "For eg:", "FOR_EG": "For eg:",
"FRIENDLY": { "FRIENDLY": {
"TITLE": "Friendly", "TITLE": "دوستانه",
"FROM": "از", "FROM": "از",
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly." "SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
}, },
"PROFESSIONAL": { "PROFESSIONAL": {
"TITLE": "Professional", "TITLE": "حرفه ای",
"SUBTITLE": "Use only the configured business name as the sender name in the email header." "SUBTITLE": "Use only the configured business name as the sender name in the email header."
}, },
"BUSINESS_NAME": { "BUSINESS_NAME": {
"BUTTON_TEXT": "+ Configure your business name", "BUTTON_TEXT": "+ Configure your business name",
"PLACEHOLDER": "Enter your business name", "PLACEHOLDER": "نام کسب و کار خود را وارد کنید",
"SAVE_BUTTON_TEXT": "ذخیره" "SAVE_BUTTON_TEXT": "ذخیره"
} }
}, },
@ -486,7 +486,7 @@
"ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "فعال یا غیرفعال کردن فرم دریافت ایمیل از کاربر", "ENABLE_EMAIL_COLLECT_BOX_SUB_TEXT": "فعال یا غیرفعال کردن فرم دریافت ایمیل از کاربر",
"AUTO_ASSIGNMENT": "فعال کردن واگذاری خودکار گفتگو به ایجنت ها", "AUTO_ASSIGNMENT": "فعال کردن واگذاری خودکار گفتگو به ایجنت ها",
"ENABLE_CSAT": "فعال کردن رضایت مشتری", "ENABLE_CSAT": "فعال کردن رضایت مشتری",
"SENDER_NAME_SECTION": "Enable Agent Name in Email", "SENDER_NAME_SECTION": "فعال سازی نام اپراتور در ایمیل",
"ENABLE_CSAT_SUB_TEXT": "پس از پایان گفتگو ، نظرسنجی CSAT (رضایت مشتری) را فعال/غیرفعال کنید", "ENABLE_CSAT_SUB_TEXT": "پس از پایان گفتگو ، نظرسنجی CSAT (رضایت مشتری) را فعال/غیرفعال کنید",
"SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name", "SENDER_NAME_SECTION_TEXT": "Enable/Disable showing Agent's name in email, if disabled it will show business name",
"ENABLE_CONTINUITY_VIA_EMAIL": "ادامه مکالمه را از طریق ایمیل فعال کنید", "ENABLE_CONTINUITY_VIA_EMAIL": "ادامه مکالمه را از طریق ایمیل فعال کنید",
@ -498,7 +498,7 @@
"AUTO_ASSIGNMENT_SUB_TEXT": "فعال کردن یا غیرفعال کردن واگذاری خودکار گفتگوها به ایجنت های عضو این صندوق ورودی.", "AUTO_ASSIGNMENT_SUB_TEXT": "فعال کردن یا غیرفعال کردن واگذاری خودکار گفتگوها به ایجنت های عضو این صندوق ورودی.",
"HMAC_VERIFICATION": "اعتبار سنجی کاربر", "HMAC_VERIFICATION": "اعتبار سنجی کاربر",
"HMAC_DESCRIPTION": "به منظور اعتبارسنجی هویت کاربر، می‌توانید یک «شناسه_هش» برای هر کاربر ارسال کنید. می‌توانید با استفاده از «شناسه» با کلید نشان داده شده در اینجا یک هش HMAC sha256 ایجاد کنید.", "HMAC_DESCRIPTION": "به منظور اعتبارسنجی هویت کاربر، می‌توانید یک «شناسه_هش» برای هر کاربر ارسال کنید. می‌توانید با استفاده از «شناسه» با کلید نشان داده شده در اینجا یک هش HMAC sha256 ایجاد کنید.",
"HMAC_LINK_TO_DOCS": "You can read more here.", "HMAC_LINK_TO_DOCS": "از اینجا می توانید بیشتر بخوانید.",
"HMAC_MANDATORY_VERIFICATION": "مجبور کردن تأیید اعتبار هویت کاربر", "HMAC_MANDATORY_VERIFICATION": "مجبور کردن تأیید اعتبار هویت کاربر",
"HMAC_MANDATORY_DESCRIPTION": "اگر فعال باشد، درخواست‌هایی که «identifier_hash» را ندارند رد می‌شوند.", "HMAC_MANDATORY_DESCRIPTION": "اگر فعال باشد، درخواست‌هایی که «identifier_hash» را ندارند رد می‌شوند.",
"INBOX_IDENTIFIER": "شناسه صندوق ورودی", "INBOX_IDENTIFIER": "شناسه صندوق ورودی",
@ -674,7 +674,7 @@
"PLACE_HOLDER": "دسترسی به ما ساده است. هر سوالی پیش آمد همینجا از ما بپرسید." "PLACE_HOLDER": "دسترسی به ما ساده است. هر سوالی پیش آمد همینجا از ما بپرسید."
}, },
"REPLY_TIME": { "REPLY_TIME": {
"LABEL": "زمان پاسخ", "LABEL": "زمان پاسخ دهی",
"IN_A_FEW_MINUTES": "در چند دقیقه", "IN_A_FEW_MINUTES": "در چند دقیقه",
"IN_A_FEW_HOURS": "در چند ساعت", "IN_A_FEW_HOURS": "در چند ساعت",
"IN_A_DAY": "در یک روز" "IN_A_DAY": "در یک روز"
@ -712,12 +712,12 @@
"CHAT": "گفتگو" "CHAT": "گفتگو"
}, },
"REPLY_TIME": { "REPLY_TIME": {
"IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ می دهند", "IN_A_FEW_MINUTES": "معمولاً در عرض چند دقیقه پاسخ دهی انجام می شود",
"IN_A_FEW_HOURS": "معمولاً در عرض چند ساعت پاسخ می دهند", "IN_A_FEW_HOURS": "معمولاً در عرض چند ساعت پاسخ می دهند",
"IN_A_DAY": "به طور معمول در یک روز پاسخ می دهند" "IN_A_DAY": "به طور معمول در یک روز پاسخ دهی انجام می شود"
}, },
"FOOTER": { "FOOTER": {
"START_CONVERSATION_BUTTON_TEXT": "شروع گفتگو", "START_CONVERSATION_BUTTON_TEXT": "گفتگو را شروع کنید",
"CHAT_INPUT_PLACEHOLDER": "پیام خود را وارد کنید" "CHAT_INPUT_PLACEHOLDER": "پیام خود را وارد کنید"
}, },
"BODY": { "BODY": {

View File

@ -131,7 +131,7 @@
"KEY_PLACEHOLDER": "Enter your OpenAI API key", "KEY_PLACEHOLDER": "Enter your OpenAI API key",
"BUTTONS": { "BUTTONS": {
"NEED_HELP": "به کمک احتياج داری؟", "NEED_HELP": "به کمک احتياج داری؟",
"DISMISS": "Dismiss", "DISMISS": "نادیده بگیر",
"FINISH": "Finish Setup" "FINISH": "Finish Setup"
}, },
"DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.", "DISMISS_MESSAGE": "You can setup OpenAI integration later Whenever you want.",

View File

@ -46,7 +46,7 @@
"DISMISS": "Dismiss suggestion" "DISMISS": "Dismiss suggestion"
}, },
"POWERED_BY": "Chatwoot AI", "POWERED_BY": "Chatwoot AI",
"DISMISS": "Dismiss", "DISMISS": "نادیده بگیر",
"ADD_SELECTED_LABELS": "Add selected labels", "ADD_SELECTED_LABELS": "Add selected labels",
"ADD_SELECTED_LABEL": "Add selected label", "ADD_SELECTED_LABEL": "Add selected label",
"ADD_ALL_LABELS": "Add all labels" "ADD_ALL_LABELS": "Add all labels"

View File

@ -34,7 +34,7 @@
"ERROR": "Inbox is required" "ERROR": "Inbox is required"
}, },
"MESSAGE": { "MESSAGE": {
"LABEL": "Message", "LABEL": "Viesti",
"PLACEHOLDER": "Please enter the message of campaign", "PLACEHOLDER": "Please enter the message of campaign",
"ERROR": "Message is required" "ERROR": "Message is required"
}, },
@ -88,7 +88,7 @@
"404": "There are no campaigns created for this inbox.", "404": "There are no campaigns created for this inbox.",
"TABLE_HEADER": { "TABLE_HEADER": {
"TITLE": "Title", "TITLE": "Title",
"MESSAGE": "Message", "MESSAGE": "Viesti",
"INBOX": "Inbox", "INBOX": "Inbox",
"STATUS": "Tila", "STATUS": "Tila",
"SENDER": "Sender", "SENDER": "Sender",

View File

@ -1,75 +1,75 @@
{ {
"CANNED_MGMT": { "CANNED_MGMT": {
"HEADER": "Tallennetut vastaukset", "HEADER": "Tallennetut vastaukset",
"HEADER_BTN_TXT": "Lisää tallennettu vastaus", "HEADER_BTN_TXT": "Add canned response",
"LOADING": "Haetaan tallennettuja vastauksia", "LOADING": "Fetching canned responses...",
"SEARCH_404": "Tätä hakua vastaavia kohteita ei löydy", "SEARCH_404": "Tätä hakua vastaavia kohteita ei löydy.",
"SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are saved reply templates which can be used to quickly send out a reply to a conversation. </p><p> For creating a Canned Response, just click on the <b>Add Canned Response</b>. You can also edit or delete an existing Canned Response by clicking on the Edit or Delete button </p><p> Canned responses are used with the help of <b>Short Codes</b>. Agents can access canned responses while on a chat by typing <b>'/'</b> followed by the short code. </p>", "SIDEBAR_TXT": "<p><b>Canned Responses</b> </p><p> Canned Responses are pre-written reply templates that help you quickly respond to a conversation. To insert a canned response during a chat, agents can type a short code preceded by a '/' character. </p><p> You can manage your canned responses from this page or create new ones using the \"Add canned response\" button.</p><p>Open the <a target=\"_blank\" href=\"https://www.chatwoot.com/hc/chatwoot-user-guide-cloud-version/articles/1677501325-how-to-create-saved-reply-templates-with-canned-responses\">Canned Responses handbook</a> in another tab for a helping hand.</p><p>Also, check out the all-new <a href=\"https://www.chatwoot.com/tools/canned-responses-library\" target=\"_blank\">Canned Responses Library</a>.</p>",
"LIST": { "LIST": {
"404": "Tällä tilillä ei ole tallennettuja vastauksia.", "404": "Tällä tilillä ei ole tallennettuja vastauksia.",
"TITLE": "Hallitse tallennettuja vastauksia", "TITLE": "Hallitse tallennettuja vastauksia",
"DESC": "Tallennetut vastaukset ovat ennalta valmisteltuja vastausmalleja, joita voidaan käyttää nopeasti lähettää vastaukset tukipyyntöihin.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Oikotie", "Short code",
"Sisältö", "Sisältö",
"Toiminnot" "Toiminnot"
] ]
}, },
"ADD": { "ADD": {
"TITLE": "Lisää tallennettu vastaus", "TITLE": "Add canned response",
"DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.",
"CANCEL_BUTTON_TEXT": "Peruuta", "CANCEL_BUTTON_TEXT": "Peruuta",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Oikotie", "LABEL": "Short code",
"PLACEHOLDER": "Please enter a short code", "PLACEHOLDER": "Please enter a short code.",
"ERROR": "Oikotie on vaadittu" "ERROR": "Short Code is required."
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Sisältö", "LABEL": "Viesti",
"PLACEHOLDER": "Ole hyvä ja syötä sisältö", "PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Sisältö on vaadittu" "ERROR": "Message is required."
}, },
"SUBMIT": "Lähetä" "SUBMIT": "Lähetä"
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus lisätty onnistuneesti", "SUCCESS_MESSAGE": "Canned response added successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen" "ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
} }
}, },
"EDIT": { "EDIT": {
"TITLE": "Muokkaa tallennettua vastausta", "TITLE": "Edit canned response",
"CANCEL_BUTTON_TEXT": "Peruuta", "CANCEL_BUTTON_TEXT": "Peruuta",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Oikotie", "LABEL": "Short code",
"PLACEHOLDER": "Ole hyvä ja syötä oikotie", "PLACEHOLDER": "Please enter a shortcode.",
"ERROR": "Oikotie on vaadittu" "ERROR": "Short code is required."
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Sisältö", "LABEL": "Viesti",
"PLACEHOLDER": "Ole hyvä ja syötä sisältö", "PLACEHOLDER": "Please write the message you want to save as a template to use later.",
"ERROR": "Sisältö on vaadittu" "ERROR": "Message is required."
}, },
"SUBMIT": "Lähetä" "SUBMIT": "Lähetä"
}, },
"BUTTON_TEXT": "Muokkaa", "BUTTON_TEXT": "Muokkaa",
"API": { "API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus päivitetty onnistuneesti", "SUCCESS_MESSAGE": "Canned response is updated successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen" "ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
} }
}, },
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Poista", "BUTTON_TEXT": "Poista",
"API": { "API": {
"SUCCESS_MESSAGE": "Tallennettu vastaus poistettu onnistuneesti", "SUCCESS_MESSAGE": "Canned response deleted successfully.",
"ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen" "ERROR_MESSAGE": "Yhteyden muodostaminen Woot-palvelimelle ei onnistunut, yritä myöhemmin uudelleen"
}, },
"CONFIRM": { "CONFIRM": {
"TITLE": "Vahvista poistaminen", "TITLE": "Confirm deletion",
"MESSAGE": "Oletko varma että haluat poistaa ", "MESSAGE": "Oletko varma että haluat poistaa ",
"YES": "Kyllä, poista ", "YES": "Yes, delete ",
"NO": "Ei, säilytä " "NO": "No, keep "
} }
} }
} }

View File

@ -200,7 +200,7 @@
"ERROR": "Subject can't be empty" "ERROR": "Subject can't be empty"
}, },
"MESSAGE": { "MESSAGE": {
"LABEL": "Message", "LABEL": "Viesti",
"PLACEHOLDER": "Write your message here", "PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty" "ERROR": "Message can't be empty"
}, },
@ -309,20 +309,20 @@
}, },
"VALUE": { "VALUE": {
"LABEL": "Attribute value", "LABEL": "Attribute value",
"PLACEHOLDER": "Eg: 11901 " "PLACEHOLDER": "Esim: 11901 "
}, },
"ADD": { "ADD": {
"TITLE": "Luo uusi määrite ", "TITLE": "Luo uusi määrite ",
"SUCCESS": "Attribute added successfully", "SUCCESS": "Attribuutin lisäys onnistui",
"ERROR": "Unable to add attribute. Please try again later" "ERROR": "Attribuuttia ei voitu lisätä. Yritä myöhemmin uudelleen"
}, },
"UPDATE": { "UPDATE": {
"SUCCESS": "Attribute updated successfully", "SUCCESS": "Tunniste päivitetty onnistuneesti",
"ERROR": "Unable to update attribute. Please try again later" "ERROR": "Attribuuttia ei voida päivittää. Yritä myöhemmin uudelleen"
}, },
"DELETE": { "DELETE": {
"SUCCESS": "Attribute deleted successfully", "SUCCESS": "Attribuutin poisto onnistui",
"ERROR": "Unable to delete attribute. Please try again later" "ERROR": "Attribuuttia ei voida poistaa. Yritä uudelleen myöhemmin"
}, },
"ATTRIBUTE_SELECT": { "ATTRIBUTE_SELECT": {
"TITLE": "Lisää määritteitä", "TITLE": "Lisää määritteitä",
@ -331,15 +331,15 @@
}, },
"ATTRIBUTE_TYPE": { "ATTRIBUTE_TYPE": {
"LIST": { "LIST": {
"PLACEHOLDER": "Select value", "PLACEHOLDER": "Valitse arvo",
"SEARCH_INPUT_PLACEHOLDER": "Search value", "SEARCH_INPUT_PLACEHOLDER": "Etsi arvo",
"NO_RESULT": "Tuloksia ei löytynyt" "NO_RESULT": "Tuloksia ei löytynyt"
} }
} }
}, },
"VALIDATIONS": { "VALIDATIONS": {
"REQUIRED": "Valid value is required", "REQUIRED": "Kelvollinen arvo vaaditaan",
"INVALID_URL": "Invalid URL" "INVALID_URL": "Virheellinen URL"
} }
}, },
"MERGE_CONTACTS": { "MERGE_CONTACTS": {
@ -355,7 +355,7 @@
"HELP_LABEL": "Säilytetään" "HELP_LABEL": "Säilytetään"
}, },
"SUMMARY": { "SUMMARY": {
"TITLE": "Summary", "TITLE": "Yhteenveto",
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.", "DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>." "ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
}, },

View File

@ -270,17 +270,17 @@
"CONVERSATION_CUSTOM_ATTRIBUTES": { "CONVERSATION_CUSTOM_ATTRIBUTES": {
"ADD_BUTTON_TEXT": "Create attribute", "ADD_BUTTON_TEXT": "Create attribute",
"UPDATE": { "UPDATE": {
"SUCCESS": "Attribute updated successfully", "SUCCESS": "Tunniste päivitetty onnistuneesti",
"ERROR": "Unable to update attribute. Please try again later" "ERROR": "Attribuuttia ei voida päivittää. Yritä myöhemmin uudelleen"
}, },
"ADD": { "ADD": {
"TITLE": "Add", "TITLE": "Add",
"SUCCESS": "Attribute added successfully", "SUCCESS": "Attribuutin lisäys onnistui",
"ERROR": "Unable to add attribute. Please try again later" "ERROR": "Attribuuttia ei voitu lisätä. Yritä myöhemmin uudelleen"
}, },
"DELETE": { "DELETE": {
"SUCCESS": "Attribute deleted successfully", "SUCCESS": "Attribuutin poisto onnistui",
"ERROR": "Unable to delete attribute. Please try again later" "ERROR": "Attribuuttia ei voida poistaa. Yritä uudelleen myöhemmin"
}, },
"ATTRIBUTE_SELECT": { "ATTRIBUTE_SELECT": {
"TITLE": "Lisää määritteitä", "TITLE": "Lisää määritteitä",

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Poista"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Nimi", "LABEL": "Nimi",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Nimi", "LABEL": "Nimi",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {

View File

@ -410,7 +410,7 @@
"FOR_EG": "For eg:", "FOR_EG": "For eg:",
"FRIENDLY": { "FRIENDLY": {
"TITLE": "Friendly", "TITLE": "Friendly",
"FROM": "from", "FROM": "lähettäjä",
"SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly." "SUBTITLE": "Add the name of the agent who sent the reply in the sender name to make it friendly."
}, },
"PROFESSIONAL": { "PROFESSIONAL": {

View File

@ -19,7 +19,7 @@
"BOT_LABEL": "Botti", "BOT_LABEL": "Botti",
"READ_MORE": "Read more", "READ_MORE": "Read more",
"WROTE": "wrote:", "WROTE": "wrote:",
"FROM": "from", "FROM": "lähettäjä",
"EMAIL": "sähköposti" "EMAIL": "sähköposti"
} }
} }

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Rechercher des émojis", "PLACEHOLDER": "Rechercher des émojis",
"NOT_FOUND": "Aucun émoji ne correspond à votre recherche" "NOT_FOUND": "Aucun émoji ne correspond à votre recherche",
"REMOVE": "Supprimer"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Nom", "LABEL": "Nom",
"PLACEHOLDER": "Nom de la catégorie", "PLACEHOLDER": "Nom de la catégorie",
"HELP_TEXT": "Le nom de la catégorie sera utilisé dans le portail public pour classer les articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Le nom est requis" "ERROR": "Le nom est requis"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Nom", "LABEL": "Nom",
"PLACEHOLDER": "Nom de la catégorie", "PLACEHOLDER": "Nom de la catégorie",
"HELP_TEXT": "Le nom de la catégorie sera utilisé dans le portail public pour classer les articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Le nom est requis" "ERROR": "Le nom est requis"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "חפש אימוג'י", "PLACEHOLDER": "חפש אימוג'י",
"NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך" "NOT_FOUND": "אין אמוג'י שתואם את החיפוש שלך",
"REMOVE": "הסר"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "שם", "LABEL": "שם",
"PLACEHOLDER": "שם קטגוריה", "PLACEHOLDER": "שם קטגוריה",
"HELP_TEXT": "שם הקטגוריה ישמש בפורטל הפונה לציבור כדי לסווג מאמרים.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "שם שדה חובה" "ERROR": "שם שדה חובה"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "שם", "LABEL": "שם",
"PLACEHOLDER": "שם קטגוריה", "PLACEHOLDER": "שם קטגוריה",
"HELP_TEXT": "שם הקטגוריה ישמש בפורטל הפונה לציבור כדי לסווג מאמרים.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "שם שדה חובה" "ERROR": "שם שדה חובה"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
} }
} }

View File

@ -365,7 +365,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {
@ -396,7 +396,7 @@
"NAME": { "NAME": {
"LABEL": "Name", "LABEL": "Name",
"PLACEHOLDER": "Category name", "PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.", "HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required" "ERROR": "Name is required"
}, },
"SLUG": { "SLUG": {

View File

@ -1,6 +1,7 @@
{ {
"EMOJI": { "EMOJI": {
"PLACEHOLDER": "Search emojis", "PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search" "NOT_FOUND": "No emoji match your search",
"REMOVE": "Izbaci"
} }
} }

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