Compare commits

..
Author SHA1 Message Date
Vishnu Narayanan cd016c5ad4 chore: log webhook response data on error 2023-11-24 14:18:13 +05:30
810 changed files with 9542 additions and 15656 deletions
+9 -12
View File
@@ -1,14 +1,10 @@
ARG VARIANT ARG VARIANT=ubuntu-20.04
FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT}
FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT}
ARG NODE_VERSION
ARG RUBY_VERSION
ARG USER_UID
ARG USER_GID
# Update args in docker-compose.yaml to set the UID/GID of the "vscode" user. # Update args in docker-compose.yaml to set the UID/GID of the "vscode" user.
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \
groupmod --gid $USER_GID vscode \ groupmod --gid $USER_GID vscode \
&& usermod --uid $USER_UID --gid $USER_GID vscode \ && usermod --uid $USER_UID --gid $USER_GID vscode \
@@ -31,10 +27,10 @@ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
tmux \ tmux \
zsh \ zsh \
git-flow \ git-flow \
npm \ npm
libyaml-dev
# Install rbenv and ruby # Install rbenv and ruby
ARG RUBY_VERSION="3.2.2"
RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \
&& echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \
&& echo 'eval "$(rbenv init -)"' >> ~/.bashrc && echo 'eval "$(rbenv init -)"' >> ~/.bashrc
@@ -69,7 +65,8 @@ COPY Gemfile Gemfile.lock ./
RUN gem install bundler && bundle install RUN gem install bundler && bundle install
# set up node js # set up node js
RUN npm install n -g && \ RUN npm install npm@latest -g && \
n $NODE_VERSION npm install n -g && \
n latest
RUN npm install --global yarn RUN npm install --global yarn
RUN yarn RUN yarn
+8 -18
View File
@@ -5,30 +5,19 @@
version: '3' version: '3'
services: services:
base:
build:
context: ..
dockerfile: .devcontainer/Dockerfile.base
args:
VARIANT: "ubuntu-22.04"
NODE_VERSION: "20.9.0"
RUBY_VERSION: "3.2.2"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000"
USER_GID: "1000"
image: base:latest
app: app:
build: build:
context: .. context: ..
dockerfile: .devcontainer/Dockerfile dockerfile: .devcontainer/Dockerfile
args: args:
VARIANT: "ubuntu-22.04" # Update 'VARIANT' to pick a Ruby version: https://github.com/microsoft/vscode-dev-containers/tree/main/containers/ruby
NODE_VERSION: "20.9.0" VARIANT: 3
RUBY_VERSION: "3.2.2" # [Choice] Install Node.js
INSTALL_NODE: "true"
NODE_VERSION: "lts/*"
# On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000.
USER_UID: "1000" USER_UID: 1000
USER_GID: "1000" USER_GID: 1000
volumes: volumes:
- ..:/workspace:cached - ..:/workspace:cached
@@ -64,3 +53,4 @@ services:
volumes: volumes:
postgres-data: postgres-data:
redis-data: redis-data:
@@ -19,6 +19,5 @@ jobs:
- name: Build the Codespace Base Image - name: Build the Codespace Base Image
run: | run: |
docker-compose -f .devcontainer/docker-compose.yml build base docker build . -t ghcr.io/chatwoot/chatwoot_codespace:latest -f .devcontainer/Dockerfile.base
docker tag base:latest ghcr.io/chatwoot/chatwoot_codespace:latest
docker push ghcr.io/chatwoot/chatwoot_codespace:latest docker push ghcr.io/chatwoot/chatwoot_codespace:latest
+8 -8
View File
@@ -75,8 +75,8 @@ gem 'jwt'
gem 'pundit' gem 'pundit'
# super admin # super admin
gem 'administrate', '>= 0.19.0' gem 'administrate', '>= 0.19.0'
gem 'administrate-field-active_storage', '>= 1.0.1' gem 'administrate-field-active_storage'
gem 'administrate-field-belongs_to_search', '>= 0.9.0' gem 'administrate-field-belongs_to_search'
##--- gems for pubsub service ---## ##--- gems for pubsub service ---##
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/ # https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
@@ -109,14 +109,14 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', 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.14.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.14.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.12.0' gem 'sidekiq-cron', '>= 1.11.0'
##-- Push notification service --## ##-- Push notification service --##
gem 'fcm' gem 'fcm'
@@ -162,7 +162,7 @@ gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.1' gem 'audited', '~> 5.4', '>= 5.4.1'
# need for google auth # need for google auth
gem 'omniauth', '>= 2.1.2' gem 'omniauth'
gem 'omniauth-google-oauth2' gem 'omniauth-google-oauth2'
gem 'omniauth-rails_csrf_protection', '~> 1.0' gem 'omniauth-rails_csrf_protection', '~> 1.0'
@@ -198,7 +198,7 @@ group :development do
gem 'squasher' gem 'squasher'
# profiling # profiling
gem 'rack-mini-profiler', '>= 3.2.0', 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 # Should install the associated chrome extension to view query logs
gem 'meta_request' gem 'meta_request'
@@ -224,7 +224,7 @@ group :development, :test do
gem 'byebug', platform: :mri gem 'byebug', platform: :mri
gem 'climate_control' gem 'climate_control'
gem 'debug', '~> 1.8' gem 'debug', '~> 1.8'
gem 'factory_bot_rails', '>= 6.4.3' gem 'factory_bot_rails'
gem 'listen' gem 'listen'
gem 'mock_redis' gem 'mock_redis'
gem 'pry-rails' gem 'pry-rails'
+40 -40
View File
@@ -113,13 +113,13 @@ GEM
kaminari (>= 1.0) kaminari (>= 1.0)
sassc-rails (~> 2.1) sassc-rails (~> 2.1)
selectize-rails (~> 0.6) selectize-rails (~> 0.6)
administrate-field-active_storage (1.0.1) administrate-field-active_storage (0.4.2)
administrate (>= 0.2.2) administrate (>= 0.2.2)
rails (>= 7.0) rails (>= 7.0)
administrate-field-belongs_to_search (0.9.0) administrate-field-belongs_to_search (0.8.0)
administrate (>= 0.3, < 1.0) administrate (>= 0.3, < 1.0)
jbuilder (~> 2) jbuilder (~> 2)
rails (>= 4.2, < 7.2) rails (>= 4.2, < 7.1)
selectize-rails (~> 0.6) selectize-rails (~> 0.6)
annotate (3.2.0) annotate (3.2.0)
activerecord (>= 3.2, < 8.0) activerecord (>= 3.2, < 8.0)
@@ -183,7 +183,7 @@ GEM
activerecord (>= 5.a) activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0) database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1) database_cleaner-core (2.0.1)
date (3.3.4) date (3.3.3)
ddtrace (1.11.1) ddtrace (1.11.1)
debase-ruby_core_source (>= 0.10.16, <= 3.2.0) debase-ruby_core_source (>= 0.10.16, <= 3.2.0)
libdatadog (~> 2.0.0.1.0) libdatadog (~> 2.0.0.1.0)
@@ -230,10 +230,10 @@ GEM
facebook-messenger (2.0.1) facebook-messenger (2.0.1)
httparty (~> 0.13, >= 0.13.7) httparty (~> 0.13, >= 0.13.7)
rack (>= 1.4.5) rack (>= 1.4.5)
factory_bot (6.4.5) factory_bot (6.2.1)
activesupport (>= 5.0.0) activesupport (>= 5.0.0)
factory_bot_rails (6.4.3) factory_bot_rails (6.2.0)
factory_bot (~> 6.4) factory_bot (~> 6.2.0)
railties (>= 5.0.0) railties (>= 5.0.0)
faker (3.2.0) faker (3.2.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
@@ -256,7 +256,7 @@ GEM
fcm (1.0.8) fcm (1.0.8)
faraday (>= 1.0.0, < 3.0) faraday (>= 1.0.0, < 3.0)
googleauth (~> 1) googleauth (~> 1)
ffi (1.16.3) ffi (1.15.5)
ffi-compiler (1.0.1) ffi-compiler (1.0.1)
ffi (>= 1.0.0) ffi (>= 1.0.0)
rake rake
@@ -440,7 +440,7 @@ GEM
activesupport (>= 4) activesupport (>= 4)
railties (>= 4) railties (>= 4)
request_store (~> 1.0) request_store (~> 1.0)
loofah (2.22.0) loofah (2.21.4)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.12.0) nokogiri (>= 1.12.0)
mail (2.8.1) mail (2.8.1)
@@ -472,14 +472,14 @@ GEM
activerecord (>= 5.2) activerecord (>= 5.2)
net-http-persistent (4.0.2) net-http-persistent (4.0.2)
connection_pool (~> 2.2) connection_pool (~> 2.2)
net-imap (0.4.9) net-imap (0.3.7)
date date
net-protocol net-protocol
net-pop (0.1.2) net-pop (0.1.2)
net-protocol net-protocol
net-protocol (0.2.2) net-protocol (0.2.1)
timeout timeout
net-smtp (0.4.0) net-smtp (0.3.3)
net-protocol net-protocol
netrc (0.11.0) netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.2) newrelic-sidekiq-metrics (1.6.2)
@@ -487,15 +487,15 @@ GEM
sidekiq sidekiq
newrelic_rpm (9.6.0) newrelic_rpm (9.6.0)
base64 base64
nio4r (2.7.0) nio4r (2.5.9)
nokogiri (1.16.0) nokogiri (1.15.4)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-arm64-darwin) nokogiri (1.15.4-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-x86_64-darwin) nokogiri (1.15.4-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.16.0-x86_64-linux) nokogiri (1.15.4-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
numo-narray (0.9.2.1) numo-narray (0.9.2.1)
oauth (1.1.0) oauth (1.1.0)
@@ -511,7 +511,7 @@ GEM
rack (>= 1.2, < 4) rack (>= 1.2, < 4)
snaky_hash (~> 2.0) snaky_hash (~> 2.0)
version_gem (~> 1.1) version_gem (~> 1.1)
omniauth (2.1.2) omniauth (2.1.1)
hashie (>= 3.4.6) hashie (>= 3.4.6)
rack (>= 2.2.3) rack (>= 2.2.3)
rack-protection rack-protection
@@ -553,7 +553,7 @@ GEM
pry-rails (0.3.9) pry-rails (0.3.9)
pry (>= 0.10.4) pry (>= 0.10.4)
public_suffix (5.0.1) public_suffix (5.0.1)
puma (6.4.2) puma (6.3.1)
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.3.0) pundit (2.3.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
@@ -566,10 +566,10 @@ GEM
rack (< 4) rack (< 4)
rack-cors (2.0.1) rack-cors (2.0.1)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-mini-profiler (3.2.0) rack-mini-profiler (3.1.1)
rack (>= 1.2.0) rack (>= 1.2.0)
rack-protection (3.1.0) rack-protection (3.0.6)
rack (~> 2.2, >= 2.2.4) rack
rack-proxy (0.7.6) rack-proxy (0.7.6)
rack rack
rack-test (2.1.0) rack-test (2.1.0)
@@ -610,7 +610,7 @@ GEM
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.19.0) redis-client (0.18.0)
connection_pool connection_pool
redis-namespace (1.10.0) redis-namespace (1.10.0)
redis (>= 4) redis (>= 4)
@@ -709,13 +709,13 @@ 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.14.0) sentry-rails (5.13.0)
railties (>= 5.0) railties (>= 5.0)
sentry-ruby (~> 5.14.0) sentry-ruby (~> 5.13.0)
sentry-ruby (5.14.0) sentry-ruby (5.13.0)
concurrent-ruby (~> 1.0, >= 1.0.2) concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.14.0) sentry-sidekiq (5.13.0)
sentry-ruby (~> 5.14.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)
@@ -725,7 +725,7 @@ GEM
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.12.0) sidekiq-cron (1.11.0)
fugit (~> 1.8) fugit (~> 1.8)
globalid (>= 1.0.1) globalid (>= 1.0.1)
sidekiq (>= 6) sidekiq (>= 6)
@@ -752,7 +752,7 @@ GEM
spring-watcher-listen (2.1.0) spring-watcher-listen (2.1.0)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
spring (>= 4) spring (>= 4)
sprockets (4.2.1) sprockets (4.2.0)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
rack (>= 2.2.4, < 4) rack (>= 2.2.4, < 4)
sprockets-rails (3.4.2) sprockets-rails (3.4.2)
@@ -766,11 +766,11 @@ GEM
telephone_number (1.4.20) telephone_number (1.4.20)
test-prof (1.2.1) test-prof (1.2.1)
thor (1.3.0) thor (1.3.0)
tilt (2.3.0) tilt (2.2.0)
time_diff (0.3.0) time_diff (0.3.0)
activesupport activesupport
i18n i18n
timeout (0.4.1) timeout (0.4.0)
trailblazer-option (0.1.2) trailblazer-option (0.1.2)
twilio-ruby (5.77.0) twilio-ruby (5.77.0)
faraday (>= 0.9, < 3.0) faraday (>= 0.9, < 3.0)
@@ -841,8 +841,8 @@ DEPENDENCIES
activerecord-import activerecord-import
acts-as-taggable-on acts-as-taggable-on
administrate (>= 0.19.0) administrate (>= 0.19.0)
administrate-field-active_storage (>= 1.0.1) administrate-field-active_storage
administrate-field-belongs_to_search (>= 0.9.0) administrate-field-belongs_to_search
annotate annotate
attr_extras attr_extras
audited (~> 5.4, >= 5.4.1) audited (~> 5.4, >= 5.4.1)
@@ -870,7 +870,7 @@ DEPENDENCIES
elastic-apm elastic-apm
email_reply_trimmer email_reply_trimmer
facebook-messenger facebook-messenger
factory_bot_rails (>= 6.4.3) factory_bot_rails
faker faker
fcm fcm
flag_shih_tzu flag_shih_tzu
@@ -905,7 +905,7 @@ DEPENDENCIES
neighbor neighbor
newrelic-sidekiq-metrics (>= 1.6.2) newrelic-sidekiq-metrics (>= 1.6.2)
newrelic_rpm newrelic_rpm
omniauth (>= 2.1.2) omniauth
omniauth-google-oauth2 omniauth-google-oauth2
omniauth-oauth2 omniauth-oauth2
omniauth-rails_csrf_protection (~> 1.0) omniauth-rails_csrf_protection (~> 1.0)
@@ -918,7 +918,7 @@ DEPENDENCIES
pundit pundit
rack-attack (>= 6.7.0) rack-attack (>= 6.7.0)
rack-cors rack-cors
rack-mini-profiler (>= 3.2.0) rack-mini-profiler (>= 3.1.1)
rack-timeout rack-timeout
rails (~> 7.0.8.0) rails (~> 7.0.8.0)
redis redis
@@ -935,12 +935,12 @@ DEPENDENCIES
scout_apm scout_apm
scss_lint scss_lint
seed_dump seed_dump
sentry-rails (>= 5.14.0) sentry-rails (>= 5.13.0)
sentry-ruby sentry-ruby
sentry-sidekiq (>= 5.14.0) sentry-sidekiq (>= 5.13.0)
shoulda-matchers shoulda-matchers
sidekiq (>= 7.1.3) sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.12.0) 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
+1 -1
View File
@@ -118,4 +118,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a> <a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
*Chatwoot* &copy; 2017-2024, Chatwoot Inc - Released under the MIT License. *Chatwoot* &copy; 2017-2023, Chatwoot Inc - Released under the MIT License.
+1 -1
View File
@@ -1 +1 @@
3.3.1 3.1.0
+1 -1
View File
@@ -1 +1 @@
2.7.0 2.6.0
+1 -1
View File
@@ -104,7 +104,7 @@ class ContactIdentifyAction
# TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded
@contact.discard_invalid_attrs if discard_invalid_attrs @contact.discard_invalid_attrs if discard_invalid_attrs
@contact.save! @contact.save!
Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? && !@contact.avatar.attached? Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end end
def merge_contact(base_contact, merge_contact) def merge_contact(base_contact, merge_contact)
@@ -25,6 +25,7 @@
@import 'components/flashes'; @import 'components/flashes';
@import 'components/form-actions'; @import 'components/form-actions';
@import 'components/main-content'; @import 'components/main-content';
@import 'components/navigation';
@import 'components/pagination'; @import 'components/pagination';
@import 'components/search'; @import 'components/search';
@import 'components/reports'; @import 'components/reports';
@@ -1,7 +1,7 @@
html { html {
background-color: $color-white; background-color: $color-white;
box-sizing: border-box; box-sizing: border-box;
font-size: 16px; font-size: 10px;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
@@ -16,8 +16,8 @@
.attribute-data { .attribute-data {
float: left; float: left;
margin-bottom: $base-spacing; margin-bottom: $base-spacing;
margin-left: 1.25rem; margin-left: 2rem;
width: calc(84% - 0.625rem); width: calc(84% - 1rem);
} }
.attribute--nested { .attribute--nested {
@@ -9,22 +9,22 @@
.field-unit__label { .field-unit__label {
float: left; float: left;
margin-left: 0.625rem; margin-left: 1rem;
text-align: right; text-align: right;
width: calc(15% - 0.625rem); width: calc(15% - 1rem);
} }
.field-unit__field { .field-unit__field {
float: left; float: left;
margin-left: 1.25rem; margin-left: 2rem;
max-width: 31.15rem; max-width: 50rem;
width: 100%; width: 100%;
} }
.field-unit--nested { .field-unit--nested {
border: $base-border; border: $base-border;
margin-left: 7.5%; margin-left: 7.5%;
max-width: 37.5rem; max-width: 60rem;
padding: $small-spacing; padding: $small-spacing;
width: 100%; width: 100%;
@@ -1,3 +1,3 @@
.form-actions { .form-actions {
margin-left: calc(15% + 1.25rem); margin-left: calc(15% + 2rem);
} }
@@ -13,10 +13,6 @@
table { table {
font-size: $font-size-small; font-size: $font-size-small;
} }
form {
margin-top: $space-two;
}
} }
.main-content__header { .main-content__header {
@@ -24,7 +20,7 @@
background-color: $color-white; background-color: $color-white;
border-bottom: 1px solid $color-border; border-bottom: 1px solid $color-border;
display: flex; display: flex;
min-height: 3.5rem; min-height: 5.6rem;
padding: $space-small $space-normal; padding: $space-small $space-normal;
} }
@@ -0,0 +1,88 @@
.logo-brand {
margin-bottom: $space-normal;
padding: $space-normal $space-smaller $space-small;
text-align: left;
img {
margin-bottom: $space-smaller;
max-height: 3rem;
}
}
.navigation {
background: $white;
border-right: 1px solid $color-border;
display: flex;
flex-direction: column;
font-size: $font-size-default;
font-weight: $font-weight-medium;
height: 100%;
justify-content: flex-start;
left: 0;
margin: 0;
overflow: auto;
padding: $space-normal;
position: fixed;
top: 0;
width: 21rem;
z-index: 1023;
li {
align-items: center;
display: flex;
font-size: $font-size-small;
a {
color: $color-gray;
text-decoration: none;
}
i {
min-width: $space-medium;
}
}
hr {
margin: $space-slab;
}
}
.navigation__link {
background-color: transparent;
color: $color-gray;
display: block;
line-height: 1;
margin-bottom: $space-smaller;
padding: $space-small;
&:hover {
color: $blue;
a {
color: $blue;
}
}
&.navigation__link--active {
background-color: $color-background;
border-radius: $base-border-radius;
color: $blue;
a {
color: $blue;
}
}
}
.logout {
bottom: $space-normal;
left: $space-normal;
position: fixed;
}
.app-version {
color: $color-gray;
font-size: $font-size-small;
padding-top: $space-smaller;
}
@@ -1,7 +1,7 @@
.search { .search {
margin-left: auto; margin-left: auto;
margin-right: 1.25rem; margin-right: 2rem;
max-width: 27.5rem; max-width: 44rem;
position: relative; position: relative;
width: 100%; width: 100%;
} }
@@ -1,10 +1,10 @@
// Typography // Typography
$base-font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", $base-font-family: PlusJakarta, Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif !default; sans-serif !default;
$heading-font-family: $base-font-family !default; $heading-font-family: $base-font-family !default;
$base-font-size: 16px !default; $base-font-size: 14px !default;
$base-line-height: 1.5 !default; $base-line-height: 1.5 !default;
$heading-line-height: 1.2 !default; $heading-line-height: 1.2 !default;
@@ -1,30 +1,30 @@
// Font sizes // Font sizes
$font-size-nano: 0.5rem; $font-size-nano: 0.8rem;
$font-size-micro: 0.675rem; $font-size-micro: 1.0rem;
$font-size-mini: 0.75rem; $font-size-mini: 1.2rem;
$font-size-small: 0.875rem; $font-size-small: 1.4rem;
$font-size-default: 1rem; $font-size-default: 1.6rem;
$font-size-medium: 1.125rem; $font-size-medium: 1.8rem;
$font-size-large: 1.375rem; $font-size-large: 2.2rem;
$font-size-big: 1.5rem; $font-size-big: 2.4rem;
$font-size-bigger: 1.75rem; $font-size-bigger: 3.0rem;
$font-size-mega: 2.125rem; $font-size-mega: 3.4rem;
$font-size-giga: 2.5rem; $font-size-giga: 4.0rem;
// spaces // spaces
$zero: 0; $zero: 0;
$space-micro: 0.125rem; $space-micro: 0.2rem;
$space-smaller: 0.25rem; $space-smaller: 0.4rem;
$space-small: 0.5rem; $space-small: 0.8rem;
$space-one: 0.675rem; $space-one: 1rem;
$space-slab: 0.75rem; $space-slab: 1.2rem;
$space-normal: 1rem; $space-normal: 1.6rem;
$space-two: 1.25rem; $space-two: 2.0rem;
$space-medium: 1.5rem; $space-medium: 2.4rem;
$space-large: 2rem; $space-large: 3.2rem;
$space-larger: 3rem; $space-larger: 4.8rem;
$space-jumbo: 4rem; $space-jumbo: 6.4rem;
$space-mega: 6.25rem; $space-mega: 10.0rem;
// font-weight // font-weight
$font-weight-feather: 100; $font-weight-feather: 100;
@@ -25,9 +25,7 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
build_contact_inbox build_contact_inbox
build_message build_message
end end
rescue Koala::Facebook::AuthenticationError => e rescue Koala::Facebook::AuthenticationError
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
rescue StandardError => e rescue StandardError => e
ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception ChatwootExceptionTracker.new(e, account: @inbox.account).capture_exception
@@ -110,15 +108,11 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
} }
end end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
def contact_params def contact_params
begin begin
k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook?
result = k.get_object(@sender_id) || {} result = k.get_object(@sender_id) || {}
rescue Koala::Facebook::AuthenticationError => e rescue Koala::Facebook::AuthenticationError
Rails.logger.warn("Facebook authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
raise raise
rescue Koala::Facebook::ClientError => e rescue Koala::Facebook::ClientError => e
@@ -136,6 +130,4 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
end end
process_contact_params_result(result) process_contact_params_result(result)
end end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/MethodLength
end end
@@ -20,9 +20,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
build_message build_message
end end
rescue Koala::Facebook::AuthenticationError => e rescue Koala::Facebook::AuthenticationError
Rails.logger.warn("Instagram authentication error for inbox: #{@inbox.id} with error: #{e.message}")
Rails.logger.error e
@inbox.channel.authorization_error! @inbox.channel.authorization_error!
raise raise
rescue StandardError => e rescue StandardError => e
@@ -48,10 +46,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
@outgoing_echo ? recipient_id : sender_id @outgoing_echo ? recipient_id : sender_id
end end
def message_is_unsupported?
message[:is_unsupported].present? && @messaging[:message][:is_unsupported] == true
end
def sender_id def sender_id
@messaging[:sender][:id] @messaging[:sender][:id]
end end
@@ -122,7 +116,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
end end
def message_params def message_params
params = { {
account_id: conversation.account_id, account_id: conversation.account_id,
inbox_id: conversation.inbox_id, inbox_id: conversation.inbox_id,
message_type: message_type, message_type: message_type,
@@ -133,9 +127,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
in_reply_to_external_id: message_reply_attributes in_reply_to_external_id: message_reply_attributes
} }
} }
params[:content_attributes][:is_unsupported] = true if message_is_unsupported?
params
end end
def already_sent_from_chatwoot? def already_sent_from_chatwoot?
+1 -2
View File
@@ -149,8 +149,7 @@ class Messages::MessageBuilder
content_type: @params[:content_type], content_type: @params[:content_type],
items: @items, items: @items,
in_reply_to: @in_reply_to, in_reply_to: @in_reply_to,
echo_id: @params[:echo_id], echo_id: @params[:echo_id]
source_id: @params[:source_id]
}.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params) }.merge(external_created_at).merge(automation_rule_id).merge(campaign_id).merge(template_params)
end end
end end
+6 -8
View File
@@ -1,20 +1,22 @@
class NotificationBuilder class NotificationBuilder
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!, :secondary_actor] pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
def perform def perform
return unless user_subscribed_to_notification?
build_notification build_notification
end end
private private
def current_user def secondary_actor
Current.user Current.user
end end
def user_subscribed_to_notification? def user_subscribed_to_notification?
notification_setting = user.notification_settings.find_by(account_id: account.id) notification_setting = user.notification_settings.find_by(account_id: account.id)
# added for the case where an assignee might be removed from the account but remains in conversation # added for the case where an assignee might be removed from the account but remains in conversation
return false if notification_setting.blank? return if notification_setting.blank?
return true if notification_setting.public_send("email_#{notification_type}?") return true if notification_setting.public_send("email_#{notification_type}?")
return true if notification_setting.public_send("push_#{notification_type}?") return true if notification_setting.public_send("push_#{notification_type}?")
@@ -23,15 +25,11 @@ class NotificationBuilder
end end
def build_notification def build_notification
# Create conversation_creation notification only if user is subscribed to it
return if notification_type == 'conversation_creation' && !user_subscribed_to_notification?
user.notifications.create!( user.notifications.create!(
notification_type: notification_type, notification_type: notification_type,
account: account, account: account,
primary_actor: primary_actor, primary_actor: primary_actor,
# secondary_actor is secondary_actor if present, else current_user secondary_actor: secondary_actor
secondary_actor: secondary_actor || current_user
) )
end end
end end
-8
View File
@@ -46,14 +46,6 @@ class V2::ReportBuilder
} }
end end
def short_summary
{
conversations_count: conversations.count,
avg_first_response_time: avg_first_response_time_summary,
avg_resolution_time: avg_resolution_time_summary
}
end
def conversation_metrics def conversation_metrics
if params[:type].equal?(:account) if params[:type].equal?(:account)
live_conversations live_conversations
@@ -10,18 +10,11 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
def show; end def show; end
def create def create
@agent_bot = Current.account.agent_bots.create!(permitted_params.except(:avatar_url)) @agent_bot = Current.account.agent_bots.create!(permitted_params)
process_avatar_from_url
end end
def update def update
@agent_bot.update!(permitted_params.except(:avatar_url)) @agent_bot.update!(permitted_params)
process_avatar_from_url
end
def avatar
@agent_bot.avatar.purge if @agent_bot.avatar.attached?
@agent_bot
end end
def destroy def destroy
@@ -37,10 +30,6 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
end end
def permitted_params def permitted_params
params.permit(:name, :description, :outgoing_url, :avatar, :avatar_url, :bot_type, bot_config: [:csml_content]) params.permit(:name, :description, :outgoing_url, :bot_type, bot_config: [:csml_content])
end
def process_avatar_from_url
::Avatar::AvatarFromUrlJob.perform_later(@agent_bot, params[:avatar_url]) if params[:avatar_url].present?
end end
end end
@@ -83,14 +83,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.new(permitted_params.except(:avatar_url)) @contact = Current.account.contacts.new(permitted_params.except(:avatar_url))
@contact.save! @contact.save!
@contact_inbox = build_contact_inbox @contact_inbox = build_contact_inbox
process_avatar_from_url process_avatar
end end
end end
def update def update
@contact.assign_attributes(contact_update_params) @contact.assign_attributes(contact_update_params)
@contact.save! @contact.save!
process_avatar_from_url process_avatar if permitted_params[:avatar].present? || permitted_params[:avatar_url].present?
end end
def destroy def destroy
@@ -174,7 +174,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
@contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id]) @contact = Current.account.contacts.includes(contact_inboxes: [:inbox]).find(params[:id])
end end
def process_avatar_from_url def process_avatar
::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? ::Avatar::AvatarFromUrlJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present?
end end
@@ -14,8 +14,7 @@ class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Account
def set_agent def set_agent
@agent = Current.account.users.find_by(id: params[:assignee_id]) @agent = Current.account.users.find_by(id: params[:assignee_id])
@conversation.assignee = @agent @conversation.update_assignee(@agent)
@conversation.save!
render_agent render_agent
end end
@@ -18,15 +18,6 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
end end
end end
def retry
return if message.blank?
message.update!(status: :sent, content_attributes: {})
::SendReplyJob.perform_later(message.id)
rescue StandardError => e
render_could_not_create_error(e.message)
end
def translate def translate
return head :ok if already_translated_content_available? return head :ok if already_translated_content_available?
@@ -60,26 +60,13 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
def toggle_status def toggle_status
# FIXME: move this logic into a service object if params[:status].present?
if pending_to_open_by_bot?
@conversation.bot_handoff!
elsif params[:status].present?
set_conversation_status set_conversation_status
@status = @conversation.save! @status = @conversation.save!
else else
@status = @conversation.toggle_status @status = @conversation.toggle_status
end end
assign_conversation if should_assign_conversation? assign_conversation if @conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end
def pending_to_open_by_bot?
return false unless Current.user.is_a?(AgentBot)
@conversation.status == 'pending' && params[:status] == 'open'
end
def should_assign_conversation?
@conversation.status == 'open' && Current.user.is_a?(User) && Current.user&.agent?
end end
def toggle_priority def toggle_priority
@@ -123,8 +110,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
def assign_conversation def assign_conversation
@conversation.assignee = current_user @agent = Current.account.users.find(current_user.id)
@conversation.save! @conversation.update_assignee(@agent)
end end
def conversation def conversation
@@ -1,15 +1,14 @@
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
RESULTS_PER_PAGE = 15 RESULTS_PER_PAGE = 15
include DateRangeHelper
before_action :fetch_notification, only: [:update, :destroy, :snooze] before_action :fetch_notification, only: [:update]
before_action :set_primary_actor, only: [:read_all] before_action :set_primary_actor, only: [:read_all]
before_action :set_current_page, only: [:index] before_action :set_current_page, only: [:index]
def index def index
@unread_count = notification_finder.unread_count @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
@notifications = notification_finder.perform @count = notifications.count
@count = notification_finder.count @notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
end end
def read_all def read_all
@@ -29,21 +28,11 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
render json: @notification render json: @notification
end end
def destroy
@notification.destroy
head :ok
end
def unread_count def unread_count
@unread_count = notification_finder.unread_count @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
render json: @unread_count render json: @unread_count
end end
def snooze
@notification.update(snoozed_until: parse_date_time(params[:snoozed_until].to_s)) if params[:snoozed_until]
render json: @notification
end
private private
def set_primary_actor def set_primary_actor
@@ -61,7 +50,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
@current_page = params[:page] || 1 @current_page = params[:page] || 1
end end
def notification_finder def notifications
@notification_finder ||= NotificationFinder.new(Current.user, Current.account, params) @notifications ||= current_user.notifications.where(account_id: current_account.id)
end end
end end
@@ -14,14 +14,7 @@ module AccessTokenAuthHelper
render_unauthorized('Invalid Access Token') && return if @access_token.blank? render_unauthorized('Invalid Access Token') && return if @access_token.blank?
@resource = @access_token.owner @resource = @access_token.owner
Current.user = @resource if allowed_current_user_type?(@resource) Current.user = @resource if current_user.is_a?(User)
end
def allowed_current_user_type?(resource)
return true if resource.is_a?(User)
return true if resource.is_a?(AgentBot)
false
end end
def validate_bot_access_token! def validate_bot_access_token!
@@ -9,15 +9,13 @@ class Platform::Api::V1::AgentBotsController < PlatformController
def show; end def show; end
def create def create
@resource = AgentBot.new(agent_bot_params.except(:avatar_url)) @resource = AgentBot.new(agent_bot_params)
@resource.save! @resource.save!
process_avatar_from_url
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
end end
def update def update
@resource.update!(agent_bot_params.except(:avatar_url)) @resource.update!(agent_bot_params)
process_avatar_from_url
end end
def destroy def destroy
@@ -25,11 +23,6 @@ class Platform::Api::V1::AgentBotsController < PlatformController
head :ok head :ok
end end
def avatar
@resource.avatar.purge if @resource.avatar.attached?
@resource
end
private private
def set_resource def set_resource
@@ -37,10 +30,6 @@ class Platform::Api::V1::AgentBotsController < PlatformController
end end
def agent_bot_params def agent_bot_params
params.permit(:name, :description, :account_id, :outgoing_url, :avatar, :avatar_url) params.permit(:name, :description, :account_id, :outgoing_url)
end
def process_avatar_from_url
::Avatar::AvatarFromUrlJob.perform_later(@resource, params[:avatar_url]) if params[:avatar_url].present?
end end
end end
@@ -1,7 +1,6 @@
class Public::Api::V1::Portals::BaseController < PublicController class Public::Api::V1::Portals::BaseController < PublicController
before_action :show_plain_layout before_action :show_plain_layout
before_action :set_color_scheme before_action :set_color_scheme
before_action :set_global_config
around_action :set_locale around_action :set_locale
after_action :allow_iframe_requests after_action :allow_iframe_requests
@@ -12,7 +11,11 @@ class Public::Api::V1::Portals::BaseController < PublicController
end end
def set_color_scheme def set_color_scheme
@theme_from_params = params[:theme] if %w[dark light].include?(params[:theme]) @theme = if %w[dark light].include?(params[:theme])
params[:theme]
else
'system'
end
end end
def portal def portal
@@ -61,8 +64,4 @@ class Public::Api::V1::Portals::BaseController < PublicController
portal portal
render 'public/api/v1/portals/error/404', status: :not_found render 'public/api/v1/portals/error/404', status: :not_found
end end
def set_global_config
@global_config = GlobalConfig.get('LOGO_THUMBNAIL', 'BRAND_NAME', 'BRAND_URL')
end
end end
@@ -1,41 +1,21 @@
class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
before_action :set_config
before_action :allowed_configs
def show def show
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
# ref: https://github.com/rubocop/rubocop/issues/7767 # ref: https://github.com/rubocop/rubocop/issues/7767
# rubocop:disable Style/HashTransformValues # rubocop:disable Style/HashTransformValues
@app_config = InstallationConfig.where(name: @allowed_configs) @fb_config = InstallationConfig.where(name: @allowed_configs)
.pluck(:name, :serialized_value) .pluck(:name, :serialized_value)
.map { |name, serialized_value| [name, serialized_value['value']] } .map { |name, serialized_value| [name, serialized_value['value']] }
.to_h .to_h
# rubocop:enable Style/HashTransformValues # rubocop:enable Style/HashTransformValues
@installation_configs = ConfigLoader.new.general_configs.each_with_object({}) do |config_hash, result|
result[config_hash['name']] = config_hash.except('name')
end
end end
def create def create
params['app_config'].each do |key, value| params['app_config'].each do |key, value|
next unless @allowed_configs.include?(key)
i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false) i = InstallationConfig.where(name: key).first_or_create(value: value, locked: false)
i.value = value i.value = value
i.save! i.save!
end end
# rubocop:disable Rails/I18nLocaleTexts redirect_to super_admin_app_config_url
redirect_to super_admin_settings_path, notice: 'App Configs updated successfully'
# rubocop:enable Rails/I18nLocaleTexts
end
private
def set_config
@config = params[:config]
end
def allowed_configs
@allowed_configs = %w[FB_APP_ID FB_VERIFY_TOKEN FB_APP_SECRET]
end end
end end
SuperAdmin::AppConfigsController.prepend_mod_with('SuperAdmin::AppConfigsController')
@@ -20,13 +20,4 @@ class SuperAdmin::ApplicationController < Administrate::ApplicationController
params.fetch(resource_name, {}).fetch(:direction, 'desc') params.fetch(resource_name, {}).fetch(:direction, 'desc')
) )
end end
private
def invalid_action_perfomed
# rubocop:disable Rails/I18nLocaleTexts
flash[:error] = 'Invalid action performed'
# rubocop:enable Rails/I18nLocaleTexts
redirect_back(fallback_location: root_path)
end
end end
@@ -27,8 +27,7 @@ class SuperAdmin::Devise::SessionsController < Devise::SessionsController
true true
rescue StandardError => e rescue StandardError => e
Rails.logger.error e.message @error_message = e.message
@error_message = 'Invalid credentials. Please try again.'
false false
end end
end end
@@ -1,5 +1,4 @@
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
rescue_from ActiveRecord::RecordNotUnique, :with => :invalid_action_perfomed
# Overwrite any of the RESTful controller actions to implement custom behavior # Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated. # For example, you may want to send an email after a foo is updated.
# #
@@ -1,10 +0,0 @@
class SuperAdmin::SettingsController < SuperAdmin::ApplicationController
def show; end
def refresh
Internal::CheckNewVersionsJob.perform_now
# rubocop:disable Rails/I18nLocaleTexts
redirect_to super_admin_settings_path, notice: 'Instance status refreshed'
# rubocop:enable Rails/I18nLocaleTexts
end
end
+4
View File
@@ -30,7 +30,11 @@ class AccessTokenDashboard < Administrate::BaseDashboard
# SHOW_PAGE_ATTRIBUTES # SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page. # an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[ SHOW_PAGE_ATTRIBUTES = %i[
owner
id
token token
created_at
updated_at
].freeze ].freeze
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
-1
View File
@@ -46,7 +46,6 @@ class AgentBotDashboard < Administrate::BaseDashboard
name name
description description
outgoing_url outgoing_url
access_token
].freeze ].freeze
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
-1
View File
@@ -32,7 +32,6 @@ class PlatformAppDashboard < Administrate::BaseDashboard
name name
created_at created_at
updated_at updated_at
access_token
].freeze ].freeze
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
+1 -3
View File
@@ -36,8 +36,7 @@ class UserDashboard < Administrate::BaseDashboard
updated_at: Field::DateTime, updated_at: Field::DateTime,
pubsub_token: Field::String, pubsub_token: Field::String,
type: Field::Select.with_options(collection: [nil, 'SuperAdmin']), type: Field::Select.with_options(collection: [nil, 'SuperAdmin']),
accounts: CountField, accounts: CountField
access_token: Field::HasOne
}.freeze }.freeze
# COLLECTION_ATTRIBUTES # COLLECTION_ATTRIBUTES
@@ -68,7 +67,6 @@ class UserDashboard < Administrate::BaseDashboard
updated_at updated_at
confirmed_at confirmed_at
account_users account_users
access_token
].freeze ].freeze
# FORM_ATTRIBUTES # FORM_ATTRIBUTES
-5
View File
@@ -18,9 +18,4 @@ class ContactDrop < BaseDrop
def last_name def last_name
@obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1 @obj.try(:name).try(:split).try(:last).try(:capitalize) if @obj.try(:name).try(:split).try(:size) > 1
end end
def custom_attribute
custom_attributes = @obj.try(:custom_attributes) || {}
custom_attributes.transform_keys(&:to_s)
end
end end
-5
View File
@@ -19,11 +19,6 @@ class ConversationDrop < BaseDrop
end end
end end
def custom_attribute
custom_attributes = @obj.try(:custom_attributes) || {}
custom_attributes.transform_keys(&:to_s)
end
private private
def message_sender_name(sender) def message_sender_name(sender)
+8 -17
View File
@@ -3,21 +3,13 @@ class ConversationFinder
DEFAULT_STATUS = 'open'.freeze DEFAULT_STATUS = 'open'.freeze
SORT_OPTIONS = { SORT_OPTIONS = {
'last_activity_at_asc' => %w[sort_on_last_activity_at asc], latest: 'latest',
'last_activity_at_desc' => %w[sort_on_last_activity_at desc], sort_on_created_at: 'sort_on_created_at',
'created_at_asc' => %w[sort_on_created_at asc], last_user_message_at: 'last_user_message_at',
'created_at_desc' => %w[sort_on_created_at desc], sort_on_priority: 'sort_on_priority',
'priority_asc' => %w[sort_on_priority asc], sort_on_waiting_since: 'sort_on_waiting_since'
'priority_desc' => %w[sort_on_priority desc],
'waiting_since_asc' => %w[sort_on_waiting_since asc],
'waiting_since_desc' => %w[sort_on_waiting_since desc],
# To be removed in v3.5.0
'latest' => %w[sort_on_last_activity_at desc],
'sort_on_created_at' => %w[sort_on_created_at asc],
'sort_on_priority' => %w[sort_on_priority desc],
'sort_on_waiting_since' => %w[sort_on_waiting_since asc]
}.with_indifferent_access }.with_indifferent_access
# assumptions # assumptions
# inbox_id if not given, take from all conversations, else specific to inbox # inbox_id if not given, take from all conversations, else specific to inbox
# assignee_type if not given, take 'all' # assignee_type if not given, take 'all'
@@ -167,8 +159,7 @@ class ConversationFinder
@conversations = @conversations.includes( @conversations = @conversations.includes(
:taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox :taggings, :inbox, { assignee: { avatar_attachment: [:blob] } }, { contact: { avatar_attachment: [:blob] } }, :team, :contact_inbox
) )
sort_by = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['latest']
sort_by, sort_order = SORT_OPTIONS[params[:sort_by]] || SORT_OPTIONS['last_activity_at_desc'] @conversations.send(sort_by).page(current_page)
@conversations.send(sort_by, sort_order).page(current_page).per(ENV.fetch('CONVERSATION_RESULTS_PER_PAGE', '25').to_i)
end end
end end
-47
View File
@@ -1,47 +0,0 @@
class NotificationFinder
attr_reader :current_user, :current_account, :params
RESULTS_PER_PAGE = 15
def initialize(current_user, current_account, params = {})
@current_user = current_user
@current_account = current_account
@params = params
set_up
end
def perform
notifications
end
def unread_count
@notifications.where(read_at: nil).count
end
def count
@notifications.count
end
private
def set_up
find_all_notifications
filter_by_status
end
def find_all_notifications
@notifications = current_user.notifications.where(account_id: @current_account.id)
end
def filter_by_status
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
end
def current_page
params[:page] || 1
end
def notifications
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: :desc)
end
end
+1 -1
View File
@@ -53,7 +53,7 @@ module Api::V1::InboxesHelper
rescue StandardError => e rescue StandardError => e
raise StandardError, e.message raise StandardError, e.message
ensure ensure
Rails.logger.error "[Api::V1::InboxesHelper] check_imap_connection failed with #{e.message}" if e.present? ChatwootExceptionTracker.new(e).capture_exception if e.present?
end end
def check_smtp_connection(channel_data, smtp) def check_smtp_connection(channel_data, smtp)
@@ -37,7 +37,7 @@ module Api::V2::Accounts::ReportsHelper
business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours]) business_hours: ActiveModel::Type::Boolean.new.cast(params[:business_hours])
} }
) )
).short_summary ).summary
end end
private private
+19 -42
View File
@@ -6,17 +6,7 @@ module PortalHelper
def generate_portal_bg(portal_color, theme) def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg' bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
"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
def generate_gradient_to_bottom(theme)
base_color = theme == 'dark' ? '#151718' : 'white'
"linear-gradient(to bottom, transparent, #{base_color})"
end
def generate_portal_hover_color(portal_color, theme)
base_color = theme == 'dark' ? '#1B1B1B' : '#F9F9F9'
"color-mix(in srgb, #{portal_color} 5%, #{base_color})"
end end
def language_name(locale) def language_name(locale)
@@ -24,48 +14,35 @@ module PortalHelper
language_map[locale] || locale language_map[locale] || locale
end end
def theme_query_string(theme) def get_theme_names(theme)
theme.present? && theme != 'system' ? "?theme=#{theme}" : '' if theme == 'light'
end I18n.t('public_portal.header.appearance.light')
elsif theme == 'dark'
def generate_home_link(portal_slug, portal_locale, theme, is_plain_layout_enabled) I18n.t('public_portal.header.appearance.dark')
if is_plain_layout_enabled
"/hc/#{portal_slug}/#{portal_locale}#{theme_query_string(theme)}"
else else
"/hc/#{portal_slug}/#{portal_locale}" I18n.t('public_portal.header.appearance.system')
end end
end end
def generate_category_link(params) def get_theme_icon(theme)
portal_slug = params[:portal_slug] if theme == 'light'
category_locale = params[:category_locale] 'icons/sun'
category_slug = params[:category_slug] elsif theme == 'dark'
theme = params[:theme] 'icons/moon'
is_plain_layout_enabled = params[:is_plain_layout_enabled]
if is_plain_layout_enabled
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}#{theme_query_string(theme)}"
else else
"/hc/#{portal_slug}/#{category_locale}/categories/#{category_slug}" 'icons/monitor'
end end
end end
def generate_article_link(portal_slug, article_slug, theme, is_plain_layout_enabled) def generate_gradient_to_bottom(theme)
if is_plain_layout_enabled "background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
"/hc/#{portal_slug}/articles/#{article_slug}#{theme_query_string(theme)}" end
else
"/hc/#{portal_slug}/articles/#{article_slug}" def generate_article_link(portal_slug, article_slug, theme)
end "/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
end end
def render_category_content(content) def render_category_content(content)
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
end end
def thumbnail_bg_color(username)
colors = ['#6D95BA', '#A4C3C3', '#E19191']
return colors.sample if username.blank?
colors[username.length % colors.size]
end
end end
@@ -86,12 +86,6 @@ class MessageApi extends ApiClient {
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`); return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
} }
retry(conversationID, messageId) {
return axios.post(
`${this.url}/${conversationID}/messages/${messageId}/retry`
);
}
getPreviousMessages({ conversationId, after, before }) { getPreviousMessages({ conversationId, after, before }) {
const params = { before }; const params = { before };
if (after && Number(after) !== Number(before)) { if (after && Number(after) !== Number(before)) {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -1,8 +1,39 @@
@import 'shared/assets/fonts/inter'; @import '../variables';
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
body { .superadmin-body {
font-family: Inter, -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important; background: var(--color-background);
.hero--title {
font-size: var(--font-size-mega);
font-weight: var(--font-weight-light);
margin-top: var(--space-large);
}
.update-subscription--checkbox {
display: flex;
input {
line-height: 1.5;
margin-right: var(--space-one);
margin-top: var(--space-smaller);
}
label {
font-size: var(--font-size-small);
line-height: 1.5;
margin-bottom: var(--space-normal);
}
}
} }
.alert-box {
background-color: var(--r-500);
border-radius: 5px;
color: var(--color-white);
font-size: 14px;
margin-bottom: 14px;
padding: 10px;
text-align: center;
}
@@ -0,0 +1,3 @@
@import 'shared/assets/fonts/plus-jakarta';
@import '../variables';
@import '~shared/assets/stylesheets/ionicons';
@@ -76,11 +76,7 @@
&.left { &.left {
.bubble { .bubble {
@apply rounded-r-lg rounded-l mr-auto break-words; @apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 rounded-r-lg rounded-l mr-auto break-words;
&:not(.is-unsupported) {
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50
}
&.is-image { &.is-image {
@apply rounded-lg; @apply rounded-lg;
@@ -1,7 +1,7 @@
.modal-mask { .modal-mask {
// @include flex; // @include flex;
// @include flex-align(center, middle); // @include flex-align(center, middle);
@apply flex items-center justify-center bg-modal-backdrop-light dark:bg-modal-backdrop-dark z-[9990] h-full left-0 fixed top-0 w-full; @apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full;
} }
.page-top-bar { .page-top-bar {
+48 -101
View File
@@ -126,33 +126,49 @@
@assign-team="onAssignTeamsForBulk" @assign-team="onAssignTeamsForBulk"
/> />
<div <div
ref="conversationList" ref="activeConversation"
class="conversations-list flex-1" class="conversations-list flex-1"
:class="{ 'overflow-hidden': isContextMenuOpen }" :class="{ 'overflow-hidden': isContextMenuOpen }"
> >
<virtual-list <div>
ref="conversationVirtualList" <conversation-card
:data-key="'id'" v-for="chat in conversationList"
:data-sources="conversationList" :key="chat.id"
:data-component="itemComponent" :active-label="label"
:extra-props="virtualListExtraProps" :team-id="teamId"
class="w-full overflow-auto h-full" :folders-id="foldersId"
footer-tag="div" :chat="chat"
:conversation-type="conversationType"
:show-assignee="showAssigneeInConversationCard"
:selected="isConversationSelected(chat.id)"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="onAssignAgent"
@assign-team="onAssignTeam"
@assign-label="onAssignLabels"
@update-conversation-status="toggleConversationStatus"
@context-menu-toggle="onContextMenuToggle"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</div>
<div v-if="chatListLoading" class="text-center">
<span class="spinner mt-4 mb-4" />
</div>
<woot-button
v-if="!hasCurrentPageEndReached && !chatListLoading"
variant="clear"
size="expanded"
class="load-more--button"
@click="loadMoreConversations"
> >
<template #footer> {{ $t('CHAT_LIST.LOAD_MORE_CONVERSATIONS') }}
<div v-if="chatListLoading" class="text-center"> </woot-button>
<span class="spinner mt-4 mb-4" />
</div> <p v-if="showEndOfListMessage" class="text-center text-muted p-4">
<p v-if="showEndOfListMessage" class="text-center text-muted p-4"> {{ $t('CHAT_LIST.EOF') }}
{{ $t('CHAT_LIST.EOF') }} </p>
</p>
<intersection-observer
v-if="!showEndOfListMessage && !chatListLoading"
:options="infiniteLoaderOptions"
@observed="loadMoreConversations"
/>
</template>
</virtual-list>
</div> </div>
<woot-modal <woot-modal
:show.sync="showAdvancedFilters" :show.sync="showAdvancedFilters"
@@ -175,12 +191,11 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import VirtualList from 'vue-virtual-scroll-list';
import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue'; import ConversationAdvancedFilter from './widgets/conversation/ConversationAdvancedFilter.vue';
import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue'; import ConversationBasicFilter from './widgets/conversation/ConversationBasicFilter.vue';
import ChatTypeTabs from './widgets/ChatTypeTabs.vue'; import ChatTypeTabs from './widgets/ChatTypeTabs.vue';
import ConversationItem from './ConversationItem.vue'; import ConversationCard from './widgets/conversation/ConversationCard.vue';
import timeMixin from '../mixins/time'; import timeMixin from '../mixins/time';
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import conversationMixin from '../mixins/conversations'; import conversationMixin from '../mixins/conversations';
@@ -207,20 +222,16 @@ import {
isOnUnattendedView, isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers'; } from '../store/modules/conversations/helpers/actionHelpers';
import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../helper/AnalyticsHelper/events';
import IntersectionObserver from './IntersectionObserver.vue';
export default { export default {
components: { components: {
AddCustomViews, AddCustomViews,
ChatTypeTabs, ChatTypeTabs,
// eslint-disable-next-line vue/no-unused-components ConversationCard,
ConversationItem,
ConversationAdvancedFilter, ConversationAdvancedFilter,
DeleteCustomViews, DeleteCustomViews,
ConversationBulkActions, ConversationBulkActions,
ConversationBasicFilter, ConversationBasicFilter,
IntersectionObserver,
VirtualList,
}, },
mixins: [ mixins: [
timeMixin, timeMixin,
@@ -230,20 +241,6 @@ export default {
filterMixin, filterMixin,
uiSettingsMixin, uiSettingsMixin,
], ],
provide() {
return {
// Actions to be performed on virtual list item and context menu.
selectConversation: this.selectConversation,
deSelectConversation: this.deSelectConversation,
assignAgent: this.onAssignAgent,
assignTeam: this.onAssignTeam,
assignLabels: this.onAssignLabels,
updateConversationStatus: this.toggleConversationStatus,
toggleContextMenu: this.onContextMenuToggle,
markAsUnread: this.markAsUnread,
assignPriority: this.assignPriority,
};
},
props: { props: {
conversationInbox: { conversationInbox: {
type: [String, Number], type: [String, Number],
@@ -278,7 +275,7 @@ export default {
return { return {
activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME, activeAssigneeTab: wootConstants.ASSIGNEE_TYPE.ME,
activeStatus: wootConstants.STATUS_TYPE.OPEN, activeStatus: wootConstants.STATUS_TYPE.OPEN,
activeSortBy: wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC, activeSortBy: wootConstants.SORT_BY_TYPE.LATEST,
showAdvancedFilters: false, showAdvancedFilters: false,
advancedFilterTypes: advancedFilterTypes.map(filter => ({ advancedFilterTypes: advancedFilterTypes.map(filter => ({
...filter, ...filter,
@@ -294,21 +291,6 @@ export default {
selectedInboxes: [], selectedInboxes: [],
isContextMenuOpen: false, isContextMenuOpen: false,
appliedFilter: [], appliedFilter: [],
infiniteLoaderOptions: {
root: this.$refs.conversationList,
rootMargin: '100px 0px 100px 0px',
},
itemComponent: ConversationItem,
// virtualListExtraProps is to pass the props to the conversationItem component.
virtualListExtraProps: {
label: this.label,
teamId: this.teamId,
foldersId: this.foldersId,
conversationType: this.conversationType,
showAssignee: false,
isConversationSelected: this.isConversationSelected,
},
}; };
}, },
computed: { computed: {
@@ -519,9 +501,6 @@ export default {
}, },
}, },
watch: { watch: {
teamId() {
this.updateVirtualListProps('teamId', this.teamId);
},
activeTeam() { activeTeam() {
this.resetAndFetchData(); this.resetAndFetchData();
}, },
@@ -530,22 +509,16 @@ export default {
}, },
label() { label() {
this.resetAndFetchData(); this.resetAndFetchData();
this.updateVirtualListProps('label', this.label);
}, },
conversationType() { conversationType() {
this.resetAndFetchData(); this.resetAndFetchData();
this.updateVirtualListProps('conversationType', this.conversationType);
}, },
activeFolder() { activeFolder() {
this.resetAndFetchData(); this.resetAndFetchData();
this.updateVirtualListProps('foldersId', this.foldersId);
}, },
chatLists() { chatLists() {
this.chatsOnView = this.conversationList; this.chatsOnView = this.conversationList;
}, },
showAssigneeInConversationCard(newVal) {
this.updateVirtualListProps('showAssignee', newVal);
},
}, },
mounted() { mounted() {
this.setFiltersFromUISettings(); this.setFiltersFromUISettings();
@@ -562,12 +535,6 @@ export default {
}); });
}, },
methods: { methods: {
updateVirtualListProps(key, value) {
this.virtualListExtraProps = {
...this.virtualListExtraProps,
[key]: value,
};
},
onApplyFilter(payload) { onApplyFilter(payload) {
this.resetBulkActions(); this.resetBulkActions();
this.foldersQuery = filterQueryGenerator(payload); this.foldersQuery = filterQueryGenerator(payload);
@@ -588,10 +555,7 @@ export default {
const { conversations_filter_by: filterBy = {} } = this.uiSettings; const { conversations_filter_by: filterBy = {} } = this.uiSettings;
const { status, order_by: orderBy } = filterBy; const { status, order_by: orderBy } = filterBy;
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN; this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
this.activeSortBy = this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
Object.keys(wootConstants.SORT_BY_TYPE).find(
sortField => sortField === orderBy
) || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC;
}, },
onClickOpenAddFoldersModal() { onClickOpenAddFoldersModal() {
this.showAddFoldersModal = true; this.showAddFoldersModal = true;
@@ -671,10 +635,10 @@ export default {
); );
}, },
getKeyboardListenerParams() { getKeyboardListenerParams() {
const allConversations = this.$refs.conversationList.querySelectorAll( const allConversations = this.$refs.activeConversation.querySelectorAll(
'div.conversations-list div.conversation' 'div.conversations-list div.conversation'
); );
const activeConversation = this.$refs.conversationList.querySelector( const activeConversation = this.$refs.activeConversation.querySelector(
'div.conversations-list div.conversation.active' 'div.conversations-list div.conversation.active'
); );
const activeConversationIndex = [...allConversations].indexOf( const activeConversationIndex = [...allConversations].indexOf(
@@ -730,12 +694,9 @@ export default {
fetchConversations() { fetchConversations() {
this.$store this.$store
.dispatch('fetchAllConversations', this.conversationFilters) .dispatch('fetchAllConversations', this.conversationFilters)
.then(this.emitConversationLoaded); .then(() => this.$emit('conversation-load'));
}, },
loadMoreConversations() { loadMoreConversations() {
if (this.hasCurrentPageEndReached || this.chatListLoading) {
return;
}
if (!this.hasAppliedFiltersOrActiveFolders) { if (!this.hasAppliedFiltersOrActiveFolders) {
this.fetchConversations(); this.fetchConversations();
} }
@@ -754,7 +715,7 @@ export default {
queryData: filterQueryGenerator(payload), queryData: filterQueryGenerator(payload),
page, page,
}) })
.then(this.emitConversationLoaded); .then(() => this.$emit('conversation-load'));
this.showAdvancedFilters = false; this.showAdvancedFilters = false;
}, },
fetchSavedFilteredConversations(payload) { fetchSavedFilteredConversations(payload) {
@@ -764,7 +725,7 @@ export default {
queryData: payload, queryData: payload,
page, page,
}) })
.then(this.emitConversationLoaded); .then(() => this.$emit('conversation-load'));
}, },
updateAssigneeTab(selectedTab) { updateAssigneeTab(selectedTab) {
if (this.activeAssigneeTab !== selectedTab) { if (this.activeAssigneeTab !== selectedTab) {
@@ -776,20 +737,6 @@ export default {
} }
} }
}, },
emitConversationLoaded() {
this.$emit('conversation-load');
this.$nextTick(() => {
// Addressing a known issue in the virtual list library where dynamically added items
// might not render correctly. This workaround involves a slight manual adjustment
// to the scroll position, triggering the list to refresh its rendering.
const virtualList = this.$refs.conversationVirtualList;
const scrollToOffset = virtualList?.scrollToOffset;
const currentOffset = virtualList?.getOffset() || 0;
if (scrollToOffset) {
scrollToOffset(currentOffset + 1);
}
});
},
resetBulkActions() { resetBulkActions() {
this.selectedConversations = []; this.selectedConversations = [];
this.selectedInboxes = []; this.selectedInboxes = [];
@@ -1,72 +0,0 @@
<template>
<conversation-card
:key="source.id"
:active-label="label"
:team-id="teamId"
:folders-id="foldersId"
:chat="source"
:conversation-type="conversationType"
:selected="isConversationSelected(source.id)"
:show-assignee="showAssignee"
:enable-context-menu="true"
@select-conversation="selectConversation"
@de-select-conversation="deSelectConversation"
@assign-agent="assignAgent"
@assign-team="assignTeam"
@assign-label="assignLabels"
@update-conversation-status="updateConversationStatus"
@context-menu-toggle="toggleContextMenu"
@mark-as-unread="markAsUnread"
@assign-priority="assignPriority"
/>
</template>
<script>
import ConversationCard from './widgets/conversation/ConversationCard.vue';
export default {
components: {
ConversationCard,
},
inject: [
'selectConversation',
'deSelectConversation',
'assignAgent',
'assignTeam',
'assignLabels',
'updateConversationStatus',
'toggleContextMenu',
'markAsUnread',
'assignPriority',
],
props: {
source: {
type: Object,
required: true,
},
teamId: {
type: [String, Number],
default: 0,
},
label: {
type: String,
default: '',
},
conversationType: {
type: String,
default: '',
},
foldersId: {
type: [String, Number],
default: 0,
},
isConversationSelected: {
type: Function,
default: () => {},
},
showAssignee: {
type: Boolean,
default: false,
},
},
};
</script>
@@ -1,34 +0,0 @@
<template>
<div ref="observedElement" class="h-6 w-full" />
</template>
<script>
export default {
props: {
options: {
type: Object,
default: () => ({ root: document, rootMargin: '100px 0 100px 0)' }),
},
},
mounted() {
this.intersectionObserver = null;
this.registerInfiniteLoader();
},
beforeDestroy() {
this.unobserveInfiniteLoadObserver();
},
methods: {
registerInfiniteLoader() {
this.intersectionObserver = new IntersectionObserver(entries => {
if (entries && entries[0].isIntersecting) {
this.$emit('observed');
}
}, this.options);
this.intersectionObserver.observe(this.$refs.observedElement);
},
unobserveInfiniteLoadObserver() {
this.intersectionObserver.unobserve(this.$refs.observedElement);
},
},
};
</script>
@@ -1,33 +1,24 @@
<template> <template>
<transition name="network-notification-fade" tag="div"> <transition name="network-notification-fade" tag="div">
<div v-show="showNotification" class="fixed top-4 left-2 z-50 group"> <div v-show="showNotification" class="ui-notification-container">
<div <div class="ui-notification">
class="flex items-center justify-between py-1 px-2 w-full rounded-lg shadow-lg bg-yellow-200 dark:bg-yellow-700 relative" <fluent-icon icon="wifi-off" />
> <p class="ui-notification-text">
<fluent-icon {{
icon="wifi-off" useInstallationName(
class="text-yellow-700/50 dark:text-yellow-50" $t('NETWORK.NOTIFICATION.TEXT'),
size="18" globalConfig.installationName
/> )
<span }}
class="text-xs tracking-wide font-medium px-2 text-yellow-700/70 dark:text-yellow-50" </p>
> <woot-button variant="clear" size="small" @click="refreshPage">
{{ $t('NETWORK.NOTIFICATION.OFFLINE') }} {{ $t('NETWORK.BUTTON.REFRESH') }}
</span> </woot-button>
<woot-button <woot-button
:title="$t('NETWORK.BUTTON.REFRESH')" variant="smooth"
variant="clear"
size="small" size="small"
color-scheme="warning" color-scheme="warning"
icon="arrow-clockwise" icon="dismiss-circle"
class="visible transition-all duration-500 ease-in-out ml-1"
@click="refreshPage"
/>
<woot-button
variant="clear"
size="small"
color-scheme="warning"
icon="dismiss"
@click="closeNotification" @click="closeNotification"
/> />
</div> </div>
@@ -56,12 +47,7 @@ export default {
mounted() { mounted() {
window.addEventListener('offline', this.updateOnlineStatus); window.addEventListener('offline', this.updateOnlineStatus);
window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => { window.bus.$on(BUS_EVENTS.WEBSOCKET_DISCONNECT, () => {
// TODO: Remove this after completing the conversation list refetching this.updateOnlineStatus({ type: 'offline' });
// TODO: DIRTY FIX : CLEAN UP THIS WITH PROPER FIX, DELAYING THE RECONNECT FOR NOW
// THE CABLE IS FIRING IS VERY COMMON AND THUS INTERFERING USER EXPERIENCE
setTimeout(() => {
this.updateOnlineStatus({ type: 'offline' });
}, 4000);
}); });
}, },
@@ -86,3 +72,33 @@ export default {
}, },
}; };
</script> </script>
<style scoped lang="scss">
@import '~dashboard/assets/scss/mixins';
.ui-notification-container {
max-width: 25rem;
position: absolute;
right: var(--space-normal);
top: var(--space-normal);
z-index: var(--z-index-very-high);
}
.ui-notification {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
background-color: var(--y-100);
border-radius: var(--border-radius-medium);
box-shadow: var(--shadow-large);
min-width: 15rem;
padding: var(--space-normal);
}
.ui-notification-text {
margin: 0 var(--space-small);
}
</style>
@@ -84,10 +84,7 @@ import {
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings'; import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import { import { replaceVariablesInMessage } from '@chatwoot/utils';
replaceVariablesInMessage,
createTypingIndicator,
} from '@chatwoot/utils';
import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events'; import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper'; import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper'; import { uploadFile } from 'dashboard/helper/uploadHelper';
@@ -143,15 +140,6 @@ export default {
}, },
data() { data() {
return { return {
typingIndicator: createTypingIndicator(
() => {
this.$emit('typing-on');
},
() => {
this.$emit('typing-off');
},
TYPING_INDICATOR_IDLE_TIME
),
showUserMentions: false, showUserMentions: false,
showCannedMenu: false, showCannedMenu: false,
showVariables: false, showVariables: false,
@@ -650,6 +638,15 @@ export default {
hideMentions() { hideMentions() {
this.showUserMentions = false; this.showUserMentions = false;
}, },
resetTyping() {
this.$emit('typing-off');
this.idleTimer = null;
},
turnOffIdleTimer() {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
}
},
handleLineBreakWhenEnterToSendEnabled(event) { handleLineBreakWhenEnterToSendEnabled(event) {
if ( if (
hasPressedEnterAndNotCmdOrShift(event) && hasPressedEnterAndNotCmdOrShift(event) &&
@@ -669,7 +666,14 @@ export default {
} }
}, },
onKeyup() { onKeyup() {
this.typingIndicator.start(); if (!this.idleTimer) {
this.$emit('typing-on');
}
this.turnOffIdleTimer();
this.idleTimer = setTimeout(
() => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME
);
this.updateImgToolbarOnDelete(); this.updateImgToolbarOnDelete();
}, },
onKeydown(event) { onKeydown(event) {
@@ -681,7 +685,8 @@ export default {
} }
}, },
onBlur() { onBlur() {
this.typingIndicator.stop(); this.turnOffIdleTimer();
this.resetTyping();
this.$emit('blur'); this.$emit('blur');
}, },
onFocus() { onFocus() {
@@ -101,10 +101,10 @@
<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="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-900 dark:text-slate-50 bg-modal-backdrop-light dark:bg-modal-backdrop-dark" 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-900 dark:text-slate-50"> <h4 class="page-sub-title text-slate-600 dark:text-slate-200">
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }} {{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
</h4> </h4>
</div> </div>
@@ -144,7 +144,6 @@ import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { import {
ALLOWED_FILE_TYPES, ALLOWED_FILE_TYPES,
ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP, ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP,
ALLOWED_FILE_TYPES_FOR_LINE,
} from 'shared/constants/messages'; } from 'shared/constants/messages';
import VideoCallButton from '../VideoCallButton.vue'; import VideoCallButton from '../VideoCallButton.vue';
import AIAssistanceButton from '../AIAssistanceButton.vue'; import AIAssistanceButton from '../AIAssistanceButton.vue';
@@ -271,9 +270,6 @@ export default {
return this.showFileUpload || this.isNote; return this.showFileUpload || this.isNote;
}, },
showAudioRecorderButton() { showAudioRecorderButton() {
if (this.isALineChannel) {
return false;
}
// Disable audio recorder for safari browser as recording is not supported // Disable audio recorder for safari browser as recording is not supported
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test( const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
navigator.userAgent navigator.userAgent
@@ -295,9 +291,6 @@ export default {
if (this.isATwilioWhatsAppChannel) { if (this.isATwilioWhatsAppChannel) {
return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP; return ALLOWED_FILE_TYPES_FOR_TWILIO_WHATSAPP;
} }
if (this.isALineChannel) {
return ALLOWED_FILE_TYPES_FOR_LINE;
}
return ALLOWED_FILE_TYPES; return ALLOWED_FILE_TYPES;
}, },
enableDragAndDrop() { enableDragAndDrop() {
@@ -1,9 +1,5 @@
<template> <template>
<mention-box <mention-box :items="items" @mention-select="handleMentionClick">
v-if="items.length"
:items="items"
@mention-select="handleMentionClick"
>
<template slot-scope="{ item }"> <template slot-scope="{ item }">
<strong>{{ item.label }}</strong> - {{ item.description }} <strong>{{ item.label }}</strong> - {{ item.description }}
</template> </template>
@@ -34,7 +34,7 @@
type="sort" type="sort"
:selected-value="sortFilter" :selected-value="sortFilter"
:items="chatSortItems" :items="chatSortItems"
path-prefix="CHAT_LIST.SORT_ORDER_ITEMS" path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
@onChangeFilter="onChangeFilter" @onChangeFilter="onChangeFilter"
/> />
</div> </div>
@@ -58,7 +58,7 @@ export default {
return { return {
showActionsDropdown: false, showActionsDropdown: false,
chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'), chatStatusItems: this.$t('CHAT_LIST.CHAT_STATUS_FILTER_ITEMS'),
chatSortItems: this.$t('CHAT_LIST.SORT_ORDER_ITEMS'), chatSortItems: this.$t('CHAT_LIST.CHAT_SORT_FILTER_ITEMS'),
}; };
}, },
computed: { computed: {
@@ -70,9 +70,7 @@ export default {
return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN; return this.chatStatusFilter || wootConstants.STATUS_TYPE.OPEN;
}, },
sortFilter() { sortFilter() {
return ( return this.chatSortFilter || wootConstants.SORT_BY_TYPE.LATEST;
this.chatSortFilter || wootConstants.SORT_BY_TYPE.LAST_ACTIVITY_AT_DESC
);
}, },
}, },
methods: { methods: {
@@ -31,7 +31,7 @@
size="40px" size="40px"
/> />
<div <div
class="px-0 py-3 border-b group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns" class="px-0 py-3 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
> >
<div class="flex justify-between"> <div class="flex justify-between">
<inbox-name v-if="showInboxName" :inbox="inbox" /> <inbox-name v-if="showInboxName" :inbox="inbox" />
@@ -175,10 +175,6 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
enableContextMenu: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@@ -293,7 +289,6 @@ export default {
this.$emit(action, this.chat.id, this.inbox.id); this.$emit(action, this.chat.id, this.inbox.id);
}, },
openContextMenu(e) { openContextMenu(e) {
if (!this.enableContextMenu) return;
e.preventDefault(); e.preventDefault();
this.$emit('context-menu-toggle', true); this.$emit('context-menu-toggle', true);
this.contextMenu.x = e.pageX || e.clientX; this.contextMenu.x = e.pageX || e.clientX;
@@ -1,7 +1,7 @@
<template> <template>
<li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble"> <li v-if="shouldRenderMessage" :id="`message${data.id}`" :class="alignBubble">
<div :class="wrapClass"> <div :class="wrapClass">
<div v-if="isFailed && !hasOneDayPassed" class="message-failed--alert"> <div v-if="isFailed" class="message-failed--alert">
<woot-button <woot-button
v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')" v-tooltip.top-end="$t('CONVERSATION.TRY_AGAIN')"
size="tiny" size="tiny"
@@ -29,19 +29,8 @@
:message-type="data.message_type" :message-type="data.message_type"
:parent-has-attachments="hasAttachments" :parent-has-attachments="hasAttachments"
/> />
<div v-if="isUnsupported">
<template v-if="isAFacebookInbox && isInstagram">
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_INSTAGRAM') }}
</template>
<template v-else-if="isAFacebookInbox">
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_FACEBOOK') }}
</template>
<template v-else>
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE') }}
</template>
</div>
<bubble-text <bubble-text
v-else-if="data.content" v-if="data.content"
:message="message" :message="message"
:is-email="isEmailContentType" :is-email="isEmailContentType"
:display-quoted-button="displayQuotedButton" :display-quoted-button="displayQuotedButton"
@@ -159,7 +148,6 @@ import { BUS_EVENTS } from 'shared/constants/busEvents';
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
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';
import { getDayDifferenceFromNow } from 'shared/helpers/DateHelper';
export default { export default {
components: { components: {
@@ -187,14 +175,6 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isAFacebookInbox: {
type: Boolean,
default: false,
},
isInstagram: {
type: Boolean,
default: false,
},
isAWhatsAppChannel: { isAWhatsAppChannel: {
type: Boolean, type: Boolean,
default: false, default: false,
@@ -229,16 +209,11 @@ export default {
created_at: this.data.created_at || '', created_at: this.data.created_at || '',
})); }));
}, },
hasOneDayPassed() {
// Disable retry button if the message is failed and the message is older than 24 hours
return getDayDifferenceFromNow(new Date(), this.data?.created_at) >= 1;
},
shouldRenderMessage() { shouldRenderMessage() {
return ( return (
this.hasAttachments || this.hasAttachments ||
this.data.content || this.data.content ||
this.isEmailContentType || this.isEmailContentType ||
this.isUnsupported ||
this.isAnIntegrationMessage this.isAnIntegrationMessage
); );
}, },
@@ -427,7 +402,6 @@ export default {
return { return {
bubble: this.isBubble, bubble: this.isBubble,
'is-private': this.data.private, 'is-private': this.data.private,
'is-unsupported': this.isUnsupported,
'is-image': this.hasMediaAttachment('image'), 'is-image': this.hasMediaAttachment('image'),
'is-video': this.hasMediaAttachment('video'), 'is-video': this.hasMediaAttachment('video'),
'is-text': this.hasText, 'is-text': this.hasText,
@@ -436,9 +410,6 @@ export default {
'is-email': this.isEmailContentType, 'is-email': this.isEmailContentType,
}; };
}, },
isUnsupported() {
return this.contentAttributes.is_unsupported ?? false;
},
isPending() { isPending() {
return this.data.status === MESSAGE_STATUS.PROGRESS; return this.data.status === MESSAGE_STATUS.PROGRESS;
}, },
@@ -450,7 +421,11 @@ export default {
return !this.sender.type || this.sender.type === 'agent_bot'; return !this.sender.type || this.sender.type === 'agent_bot';
}, },
shouldShowContextMenu() { shouldShowContextMenu() {
return !(this.isFailed || this.isPending || this.isUnsupported); return !(this.isFailed || this.isPending);
},
errorMessage() {
const { meta } = this.data;
return meta ? meta.error : '';
}, },
showAvatar() { showAvatar() {
if (this.isOutgoing || this.isTemplate) { if (this.isOutgoing || this.isTemplate) {
@@ -552,14 +527,6 @@ export default {
> .bubble { > .bubble {
@apply min-w-[128px]; @apply min-w-[128px];
&.is-unsupported {
@apply text-xs max-w-[300px] border-dashed border border-slate-200 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-700 dark:border-slate-500;
.message-text--metadata .time {
@apply text-slate-400 dark:text-slate-300;
}
}
&.is-image, &.is-image,
&.is-video { &.is-video {
@apply p-0 overflow-hidden; @apply p-0 overflow-hidden;
@@ -572,12 +539,10 @@ export default {
> video { > video {
@apply rounded-lg; @apply rounded-lg;
} }
> video { > video {
@apply h-full w-full object-cover; @apply h-full w-full object-cover;
} }
} }
.video { .video {
@apply h-[11.25rem]; @apply h-[11.25rem];
} }
@@ -592,11 +557,9 @@ export default {
.file--icon { .file--icon {
@apply text-woot-400 dark:text-woot-400; @apply text-woot-400 dark:text-woot-400;
} }
.text-block-title { .text-block-title {
@apply text-slate-700 dark:text-slate-700; @apply text-slate-700 dark:text-slate-700;
} }
.download.button { .download.button {
@apply text-woot-400 dark:text-woot-400; @apply text-woot-400 dark:text-woot-400;
} }
@@ -605,7 +568,6 @@ export default {
&.is-private.is-text > .message-text__wrap .link { &.is-private.is-text > .message-text__wrap .link {
@apply text-woot-600 dark:text-woot-200; @apply text-woot-600 dark:text-woot-200;
} }
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node { &.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
@apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline; @apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline;
} }
@@ -616,7 +578,6 @@ export default {
.message-text--metadata .time { .message-text--metadata .time {
@apply text-violet-50 dark:text-violet-50; @apply text-violet-50 dark:text-violet-50;
} }
&.is-private .message-text--metadata .time { &.is-private .message-text--metadata .time {
@apply text-slate-400 dark:text-slate-400; @apply text-slate-400 dark:text-slate-400;
} }
@@ -20,15 +20,7 @@
icon="info" icon="info"
/> />
</template> </template>
<span v-if="message.content && isMessageSticker"> <span v-if="message.content">
<fluent-icon
size="16"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
icon="image"
/>
{{ $t('CHAT_LIST.ATTACHMENTS.image.CONTENT') }}
</span>
<span v-else-if="message.content">
{{ parsedLastMessage }} {{ parsedLastMessage }}
</span> </span>
<span v-else-if="message.attachments"> <span v-else-if="message.attachments">
@@ -96,9 +88,6 @@ export default {
attachmentMessageContent() { attachmentMessageContent() {
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`; return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
}, },
isMessageSticker() {
return this.message && this.message.content_type === 'sticker';
},
}, },
}; };
</script> </script>
@@ -32,8 +32,6 @@
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel" :is-a-whatsapp-channel="isAWhatsAppChannel"
:is-web-widget-inbox="isAWebWidgetInbox" :is-web-widget-inbox="isAWebWidgetInbox"
:is-a-facebook-inbox="isAFacebookInbox"
:is-instagram="isInstagramDM"
:inbox-supports-reply-to="inboxSupportsReplyTo" :inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)" :in-reply-to="getInReplyToMessage(message)"
/> />
@@ -56,8 +54,6 @@
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel" :is-a-whatsapp-channel="isAWhatsAppChannel"
:is-web-widget-inbox="isAWebWidgetInbox" :is-web-widget-inbox="isAWebWidgetInbox"
:is-a-facebook-inbox="isAFacebookInbox"
:is-instagram-dm="isInstagramDM"
:inbox-supports-reply-to="inboxSupportsReplyTo" :inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)" :in-reply-to="getInReplyToMessage(message)"
/> />
@@ -287,16 +283,11 @@ export default {
unreadMessageCount() { unreadMessageCount() {
return this.currentChat.unread_count || 0; return this.currentChat.unread_count || 0;
}, },
isInstagramDM() {
return this.conversationType === 'instagram_direct_message';
},
inboxSupportsReplyTo() { inboxSupportsReplyTo() {
const incoming = this.inboxHasFeature(INBOX_FEATURES.REPLY_TO); return {
const outgoing = incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING) && outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
!this.is360DialogWhatsAppChannel; };
return { incoming, outgoing };
}, },
}, },
@@ -271,17 +271,11 @@ export default {
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}), }),
currentContact() {
return this.$store.getters['contacts/getContact'](
this.currentChat.meta.sender.id
);
},
shouldShowReplyToMessage() { shouldShowReplyToMessage() {
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.is360DialogWhatsAppChannel
); );
}, },
showRichContentEditor() { showRichContentEditor() {
@@ -398,8 +392,7 @@ export default {
this.isAPIInbox || this.isAPIInbox ||
this.isAnEmailChannel || this.isAnEmailChannel ||
this.isASmsInbox || this.isASmsInbox ||
this.isATelegramChannel || this.isATelegramChannel
this.isALineChannel
); );
}, },
replyButtonLabel() { replyButtonLabel() {
@@ -502,11 +495,7 @@ export default {
return `draft-${this.conversationIdByRoute}-${this.replyType}`; return `draft-${this.conversationIdByRoute}-${this.replyType}`;
}, },
audioRecordFormat() { audioRecordFormat() {
if ( if (this.isAWhatsAppChannel || this.isAPIInbox) {
this.isAWhatsAppChannel ||
this.isAPIInbox ||
this.isATelegramChannel
) {
return AUDIO_FORMATS.OGG; return AUDIO_FORMATS.OGG;
} }
return AUDIO_FORMATS.WAV; return AUDIO_FORMATS.WAV;
@@ -514,7 +503,6 @@ export default {
messageVariables() { messageVariables() {
const variables = getMessageVariables({ const variables = getMessageVariables({
conversation: this.currentChat, conversation: this.currentChat,
contact: this.currentContact,
}); });
return variables; return variables;
}, },
@@ -636,8 +624,6 @@ export default {
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}` `${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
); );
} }
this.$track(CONVERSATION_EVENTS.INSERT_ARTICLE_LINK);
}, },
toggleRichContentEditor() { toggleRichContentEditor() {
this.updateUISettings({ this.updateUISettings({
@@ -1,10 +1,5 @@
<template> <template>
<mention-box <mention-box :items="items" @mention-select="handleVariableClick">
v-if="items.length"
type="variable"
:items="items"
@mention-select="handleVariableClick"
>
<template slot-scope="{ item }"> <template slot-scope="{ item }">
<span class="text-capitalize variable--list-label"> <span class="text-capitalize variable--list-label">
{{ item.description }} {{ item.description }}
@@ -15,7 +10,6 @@
</template> </template>
<script> <script>
import { mapGetters } from 'vuex';
import { MESSAGE_VARIABLES } from 'shared/constants/messages'; import { MESSAGE_VARIABLES } from 'shared/constants/messages';
import MentionBox from '../mentions/MentionBox.vue'; import MentionBox from '../mentions/MentionBox.vue';
@@ -28,16 +22,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters({
customAttributes: 'attributes/getAttributes',
}),
items() { items() {
return [
...this.standardAttributeVariables,
...this.customAttributeVariables,
];
},
standardAttributeVariables() {
return MESSAGE_VARIABLES.filter(variable => { return MESSAGE_VARIABLES.filter(variable => {
return ( return (
variable.label.includes(this.searchKey) || variable.label.includes(this.searchKey) ||
@@ -49,20 +34,6 @@ export default {
description: variable.label, description: variable.label,
})); }));
}, },
customAttributeVariables() {
return this.customAttributes.map(attribute => {
const attributePrefix =
attribute.attribute_model === 'conversation_attribute'
? 'conversation'
: 'contact';
return {
label: `${attributePrefix}.custom_attribute.${attribute.attribute_key}`,
key: `${attributePrefix}.custom_attribute.${attribute.attribute_key}`,
description: attribute.attribute_description,
};
});
},
}, },
methods: { methods: {
handleVariableClick(item = {}) { handleVariableClick(item = {}) {
@@ -7,11 +7,7 @@
}" }"
> >
<div v-if="!isEmail" v-dompurify-html="message" class="text-content" /> <div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
<letter <letter v-else class="text-content" :html="message" />
v-else
class="text-content bg-white dark:bg-white text-slate-900 dark:text-slate-900 p-2 rounded-[4px]"
:html="message"
/>
<button <button
v-if="showQuoteToggle" v-if="showQuoteToggle"
class="text-slate-300 dark:text-slate-300 cursor-pointer text-xs py-1" class="text-slate-300 dark:text-slate-300 cursor-pointer text-xs py-1"
@@ -46,25 +46,9 @@
</span> </span>
</div> </div>
<div <div
class="items-center flex gap-2 justify-end min-w-[8rem] sm:min-w-[15rem]" class="items-center flex gap-2 justify-end min-w-[15rem]"
@click.stop @click.stop
> >
<woot-button
v-if="isImage"
size="large"
color-scheme="secondary"
variant="clear"
icon="arrow-rotate-counter-clockwise"
@click="onRotate('counter-clockwise')"
/>
<woot-button
v-if="isImage"
size="large"
color-scheme="secondary"
variant="clear"
icon="arrow-rotate-clockwise"
@click="onRotate('clockwise')"
/>
<woot-button <woot-button
size="large" size="large"
color-scheme="secondary" color-scheme="secondary"
@@ -105,7 +89,6 @@
:key="activeAttachment.message_id" :key="activeAttachment.message_id"
:src="activeAttachment.data_url" :src="activeAttachment.data_url"
class="modal-image skip-context-menu my-0 mx-auto" class="modal-image skip-context-menu my-0 mx-auto"
:style="imageRotationStyle"
@click.stop @click.stop
/> />
<video <video
@@ -203,7 +186,6 @@ export default {
this.allAttachments.findIndex( this.allAttachments.findIndex(
attachment => attachment.message_id === this.attachment.message_id attachment => attachment.message_id === this.attachment.message_id
) || 0, ) || 0,
activeImageRotation: 0,
}; };
}, },
computed: { computed: {
@@ -250,11 +232,6 @@ export default {
const fileName = dataUrl?.split('/').pop(); const fileName = dataUrl?.split('/').pop();
return fileName || ''; return fileName || '';
}, },
imageRotationStyle() {
return {
transform: `rotate(${this.activeImageRotation}deg)`,
};
},
}, },
mounted() { mounted() {
this.setImageAndVideoSrc(this.attachment); this.setImageAndVideoSrc(this.attachment);
@@ -269,7 +246,6 @@ export default {
} }
this.activeImageIndex = index; this.activeImageIndex = index;
this.setImageAndVideoSrc(attachment); this.setImageAndVideoSrc(attachment);
this.activeImageRotation = 0;
}, },
setImageAndVideoSrc(attachment) { setImageAndVideoSrc(attachment) {
const { file_type: type } = attachment; const { file_type: type } = attachment;
@@ -304,20 +280,6 @@ export default {
link.download = `attachment.${type}`; link.download = `attachment.${type}`;
link.click(); link.click();
}, },
onRotate(type) {
if (!this.isImage) {
return;
}
const rotation = type === 'clockwise' ? 90 : -90;
// Reset rotation if it is 360
if (Math.abs(this.activeImageRotation) === 360) {
this.activeImageRotation = rotation;
} else {
this.activeImageRotation += rotation;
}
},
}, },
}; };
</script> </script>
@@ -25,7 +25,7 @@
id="file" id="file"
ref="file" ref="file"
type="file" type="file"
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp" accept="image/png, image/jpeg, image/gif"
@change="handleImageUpload" @change="handleImageUpload"
/> />
<slot /> <slot />
@@ -1,40 +1,20 @@
<template> <template>
<div <div v-if="items.length" ref="mentionsListContainer" class="mention--box">
ref="mentionsListContainer"
class="bg-white dark:bg-slate-800 rounded-md overflow-auto absolute w-full z-20 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border border-solid border-slate-100 dark:border-slate-700 mention--box"
>
<ul class="vertical dropdown menu"> <ul class="vertical dropdown menu">
<woot-dropdown-item <woot-dropdown-item
v-for="(item, index) in items" v-for="(item, index) in items"
:id="`mention-item-${index}`" :id="`mention-item-${index}`"
:key="item.key" :key="item.key"
class="!mb-0"
@mouseover="onHover(index)" @mouseover="onHover(index)"
> >
<button <woot-button
class="flex group flex-col gap-0.5 overflow-hidden cursor-pointer items-start py-2.5 px-2.5 justify-center w-full h-full text-left hover:bg-woot-50 dark:hover:bg-woot-800 border-b border-solid border-slate-100 dark:border-slate-700" class="canned-item__button"
:class="{ :variant="index === selectedIndex ? '' : 'clear'"
' bg-woot-25 dark:bg-woot-800': index === selectedIndex, :class="{ active: index === selectedIndex }"
}"
@click="onListItemSelection(index)" @click="onListItemSelection(index)"
> >
<p <strong>{{ item.label }}</strong> - {{ item.description }}
class="text-slate-900 dark:text-slate-100 group-hover:text-woot-500 dark:group-hover:text-woot-500 font-medium mb-0 text-sm overflow-hidden text-ellipsis whitespace-nowrap min-w-0 max-w-full" </woot-button>
:class="{
'text-woot-500 dark:text-woot-500': index === selectedIndex,
}"
>
{{ item.description }}
</p>
<p
class="text-slate-500 dark:text-slate-300 group-hover:text-woot-500 dark:group-hover:text-woot-500 mb-0 text-xs overflow-hidden text-ellipsis whitespace-nowrap min-w-0 max-w-full"
:class="{
'text-woot-500 dark:text-woot-500': index === selectedIndex,
}"
>
{{ variableKey(item) }}
</p>
</button>
</woot-dropdown-item> </woot-dropdown-item>
</ul> </ul>
</div> </div>
@@ -49,10 +29,6 @@ export default {
type: Array, type: Array,
default: () => {}, default: () => {},
}, },
type: {
type: String,
default: 'canned',
},
}, },
data() { data() {
return { return {
@@ -97,17 +73,28 @@ export default {
onSelect() { onSelect() {
this.$emit('mention-select', this.items[this.selectedIndex]); this.$emit('mention-select', this.items[this.selectedIndex]);
}, },
variableKey(item = {}) {
return this.type === 'variable' ? `{{${item.label}}}` : `/${item.label}`;
},
}, },
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.mention--box { .mention--box {
.dropdown-menu__item:last-child > button { @apply bg-white dark:bg-slate-700 rounded-md overflow-auto absolute w-full z-20 pt-2 px-2 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border-t border-solid border-slate-75 dark:border-slate-800;
@apply border-0;
.dropdown-menu__item:last-child {
@apply pb-1;
}
.active {
@apply text-white dark:text-white;
&:hover {
@apply bg-woot-700 dark:bg-woot-700;
}
}
.button {
@apply transition-none h-8 leading-[1.4];
} }
} }
@@ -13,14 +13,10 @@ export default {
ALL: 'all', ALL: 'all',
}, },
SORT_BY_TYPE: { SORT_BY_TYPE: {
LAST_ACTIVITY_AT_ASC: 'last_activity_at_asc', LATEST: 'latest',
LAST_ACTIVITY_AT_DESC: 'last_activity_at_desc', CREATED_AT: 'sort_on_created_at',
CREATED_AT_ASC: 'created_at_asc', PRIORITY: 'sort_on_priority',
CREATED_AT_DESC: 'created_at_desc', WATIING_SINCE: 'waiting_since',
PRIORITY_ASC: 'priority_asc',
PRIORITY_DESC: 'priority_desc',
WAITING_SINCE_ASC: 'waiting_since_asc',
WAITING_SINCE_DESC: 'waiting_since_desc',
}, },
ARTICLE_STATUS_TYPES: { ARTICLE_STATUS_TYPES: {
DRAFT: 0, DRAFT: 0,
@@ -9,7 +9,6 @@ export const CONVERSATION_EVENTS = Object.freeze({
SEARCH_CONVERSATION: 'Searched conversations', SEARCH_CONVERSATION: 'Searched conversations',
APPLY_FILTER: 'Applied filters in the conversation list', APPLY_FILTER: 'Applied filters in the conversation list',
CHANGE_PRIORITY: 'Assigned priority to a conversation', CHANGE_PRIORITY: 'Assigned priority to a conversation',
INSERT_ARTICLE_LINK: 'Inserted article into reply via article search',
}); });
export const ACCOUNT_EVENTS = Object.freeze({ export const ACCOUNT_EVENTS = Object.freeze({
@@ -10,7 +10,6 @@ export class DataManager {
async initDb() { async initDb() {
if (this.db) return this.db; if (this.db) return this.db;
const dbName = `cw-store-${this.accountId}`;
this.db = await openDB(`cw-store-${this.accountId}`, DATA_VERSION, { this.db = await openDB(`cw-store-${this.accountId}`, DATA_VERSION, {
upgrade(db) { upgrade(db) {
db.createObjectStore('cache-keys'); db.createObjectStore('cache-keys');
@@ -20,13 +19,6 @@ export class DataManager {
}, },
}); });
// Store the database name in LocalStorage
const dbNames = JSON.parse(localStorage.getItem('cw-idb-names') || '[]');
if (!dbNames.includes(dbName)) {
dbNames.push(dbName);
localStorage.setItem('cw-idb-names', JSON.stringify(dbNames));
}
return this.db; return this.db;
} }
@@ -23,7 +23,6 @@ class ActionCableConnector extends BaseActionCableConnector {
'contact.updated': this.onContactUpdate, 'contact.updated': this.onContactUpdate,
'conversation.mentioned': this.onConversationMentioned, 'conversation.mentioned': this.onConversationMentioned,
'notification.created': this.onNotificationCreated, 'notification.created': this.onNotificationCreated,
'notification.deleted': this.onNotificationDeleted,
'first.reply.created': this.onFirstReplyCreated, 'first.reply.created': this.onFirstReplyCreated,
'conversation.read': this.onConversationRead, 'conversation.read': this.onConversationRead,
'conversation.updated': this.onConversationUpdated, 'conversation.updated': this.onConversationUpdated,
@@ -196,10 +195,6 @@ class ActionCableConnector extends BaseActionCableConnector {
this.app.$store.dispatch('notifications/addNotification', data); this.app.$store.dispatch('notifications/addNotification', data);
}; };
onNotificationDeleted = data => {
this.app.$store.dispatch('notifications/deleteNotification', data);
};
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
onFirstReplyCreated = () => { onFirstReplyCreated = () => {
bus.$emit('fetch_overview_reports'); bus.$emit('fetch_overview_reports');
@@ -1,18 +1,18 @@
{ {
"FILTER": { "FILTER": {
"TITLE": "Filter conversations", "TITLE": "Filter Conversations",
"SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.", "SUBTITLE": "Add filters below and hit 'Apply filters' to filter conversations.",
"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": "Oops, looks like we can't save nothing! Please add at least one filter to save it.", "FILTER_DELETE_ERROR": "You should have atleast one filter to save",
"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": "Name your filter to refer it later.", "PLACEHOLDER": "Enter a name for this filter",
"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"
} }
} }
} }
@@ -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 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>", "SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": { "CSML_BOT_EDITOR": {
"NAME": { "NAME": {
"LABEL": "Bot name", "LABEL": "Bot Name",
"PLACEHOLDER": "Name your bot.", "PLACEHOLDER": "Give your bot a name",
"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": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.", "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.",
"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.", "ERROR_MESSAGE": "Could not update the agent bot, please try again later",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.", "DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later",
"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 delete bot. Please try again." "ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
} }
}, },
"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." "ERROR_MESSAGE": "Could not update bot, Please try again later"
} }
}, },
"TYPES": { "TYPES": {
"WEBHOOK": "Webhook bot", "WEBHOOK": "Webhook Bot",
"CSML": "CSML bot" "CSML": "CSML Bot"
} }
} }
} }
@@ -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"
} }
} }
} }
@@ -1,75 +1,75 @@
{ {
"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 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>", "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>",
"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 conversations.", "DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to tickets.",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Short code", "Short Code",
"Content", "Content",
"Actions" "Actions"
] ]
}, },
"ADD": { "ADD": {
"TITLE": "Add canned response", "TITLE": "Add Canned Response",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.", "DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
"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": "Message", "LABEL": "Content",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.", "PLACEHOLDER": "Please enter a content",
"ERROR": "Message is required." "ERROR": "Content is required"
}, },
"SUBMIT": "Submit" "SUBMIT": "Submit"
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response added successfully.", "SUCCESS_MESSAGE": "Canned Response added successfully",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again." "ERROR_MESSAGE": "Could not create canned response. Please try again later."
} }
}, },
"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": "Message", "LABEL": "Content",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.", "PLACEHOLDER": "Please enter a content",
"ERROR": "Message is required." "ERROR": "Content is required"
}, },
"SUBMIT": "Submit" "SUBMIT": "Submit"
}, },
"BUTTON_TEXT": "Edit", "BUTTON_TEXT": "Edit",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response is updated successfully.", "SUCCESS_MESSAGE": "Canned Response updated successfully",
"ERROR_MESSAGE": "Could not connect to Woot server. Please try again." "ERROR_MESSAGE": "Could not update canned response. Please try again later."
} }
}, },
"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 connect to Woot server. Please try again." "ERROR_MESSAGE": "Could not delete canned response. Please try again later."
}, },
"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 "
} }
} }
} }
@@ -51,30 +51,18 @@
"ACTIVE": "Last activity" "ACTIVE": "Last activity"
} }
}, },
"SORT_ORDER_ITEMS": { "CHAT_SORT_FILTER_ITEMS": {
"last_activity_at_asc": { "latest": {
"TEXT": "Last activity: Oldest first" "TEXT": "Last activity"
}, },
"last_activity_at_desc": { "sort_on_created_at": {
"TEXT": "Last activity: Newest first" "TEXT": "Created at"
}, },
"created_at_desc": { "sort_on_priority": {
"TEXT": "Created at: Newest first" "TEXT": "Priority"
}, },
"created_at_asc": { "sort_on_waiting_since": {
"TEXT": "Created at: Oldest first" "TEXT": "Pending Response"
},
"priority_desc": {
"TEXT": "Priority: Highest first"
},
"priority_asc": {
"TEXT": "Priority: Lowest first"
},
"waiting_since_asc": {
"TEXT": "Pending Response: Longest first"
},
"waiting_since_desc": {
"TEXT": "Pending Response: Shortest first"
} }
}, },
"ATTACHMENTS": { "ATTACHMENTS": {
@@ -41,9 +41,6 @@
"SAVE_CONTACT": "Save", "SAVE_CONTACT": "Save",
"UPLOADING_ATTACHMENTS": "Uploading attachments...", "UPLOADING_ATTACHMENTS": "Uploading attachments...",
"REPLIED_TO_STORY": "Replied to your story", "REPLIED_TO_STORY": "Replied to your story",
"UNSUPPORTED_MESSAGE": "This message is unsupported.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully", "SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again", "FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
"NO_RESPONSE": "No response", "NO_RESPONSE": "No response",
@@ -142,7 +139,6 @@
"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",
@@ -92,7 +92,7 @@
}, },
"NETWORK": { "NETWORK": {
"NOTIFICATION": { "NOTIFICATION": {
"OFFLINE": "Offline" "TEXT": "Disconnected from Chatwoot"
}, },
"BUTTON": { "BUTTON": {
"REFRESH": "Refresh" "REFRESH": "Refresh"
@@ -3,7 +3,6 @@
"HEADER": { "HEADER": {
"FILTER": "Filter by", "FILTER": "Filter by",
"SORT": "Sort by", "SORT": "Sort by",
"LOCALE": "Locale",
"SETTINGS_BUTTON": "Settings", "SETTINGS_BUTTON": "Settings",
"NEW_BUTTON": "New Article", "NEW_BUTTON": "New Article",
"DROPDOWN_OPTIONS": { "DROPDOWN_OPTIONS": {
@@ -16,12 +15,6 @@
"MINE": "My Articles", "MINE": "My Articles",
"DRAFT": "Draft Articles", "DRAFT": "Draft Articles",
"ARCHIVED": "Archived Articles" "ARCHIVED": "Archived Articles"
},
"LOCALE_SELECT": {
"TITLE": "Select locale",
"PLACEHOLDER": "Select locale",
"NO_RESULT": "No locale found",
"SEARCH_PLACEHOLDER": "Search locale"
} }
}, },
"EDIT_HEADER": { "EDIT_HEADER": {
@@ -83,9 +76,6 @@
}, },
"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",
@@ -230,10 +220,7 @@
"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",
@@ -439,21 +426,6 @@
} }
} }
}, },
"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.",
@@ -1,18 +1,18 @@
{ {
"FILTER": { "FILTER": {
"TITLE": "تصفية المحادثات", "TITLE": "تصفية المحادثات",
"SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.", "SUBTITLE": "إضافة فلاتر أدناه واضغط على 'إرسال' لتصفية المحادثات.",
"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": "إضافة عامل تصفية",
"FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.", "FILTER_DELETE_ERROR": "يجب أن يكون لديك عامل تصفية واحد على الأقل للحفظ",
"SUBMIT_BUTTON_LABEL": "تطبيق عامل التصفية", "SUBMIT_BUTTON_LABEL": "تطبيق عامل التصفية",
"UPDATE_BUTTON_LABEL": "Update folder", "UPDATE_BUTTON_LABEL": "Update folder",
"CANCEL_BUTTON_LABEL": "إلغاء", "CANCEL_BUTTON_LABEL": "إلغاء",
"CLEAR_BUTTON_LABEL": "Clear filters", "CLEAR_BUTTON_LABEL": "مسح عامل التصفية",
"EMPTY_VALUE_ERROR": "القيمة مطلوبة",
"FOLDER_LABEL": "Folder Name", "FOLDER_LABEL": "Folder Name",
"FOLDER_QUERY_LABEL": "Folder Query", "FOLDER_QUERY_LABEL": "Folder Query",
"EMPTY_VALUE_ERROR": "القيمة مطلوبة.",
"TOOLTIP_LABEL": "تصفية المحادثات", "TOOLTIP_LABEL": "تصفية المحادثات",
"QUERY_DROPDOWN_LABELS": { "QUERY_DROPDOWN_LABELS": {
"AND": "و", "AND": "و",
@@ -36,15 +36,15 @@
}, },
"ATTRIBUTES": { "ATTRIBUTES": {
"STATUS": "الحالة", "STATUS": "الحالة",
"ASSIGNEE_NAME": "Assignee name", "ASSIGNEE_NAME": "اسم المحال إليه",
"INBOX_NAME": "اسم صندوق الوارد", "INBOX_NAME": "اسم صندوق الوارد لقناة التواصل",
"TEAM_NAME": "اسم الفريق", "TEAM_NAME": "اسم الفريق",
"CONVERSATION_IDENTIFIER": "Conversation identifier", "CONVERSATION_IDENTIFIER": "معرف المحادثة",
"CAMPAIGN_NAME": "Campaign name", "CAMPAIGN_NAME": "اسم الحملة",
"LABELS": "الوسوم", "LABELS": "الوسوم",
"BROWSER_LANGUAGE": "Browser language", "BROWSER_LANGUAGE": "لغة المتصفح",
"PRIORITY": "الأولوية", "PRIORITY": "الأولوية",
"COUNTRY_NAME": "Country name", "COUNTRY_NAME": "اسم الدولة",
"REFERER_LINK": "رابط المرجع", "REFERER_LINK": "رابط المرجع",
"CUSTOM_ATTRIBUTE_LIST": "القائمة", "CUSTOM_ATTRIBUTE_LIST": "القائمة",
"CUSTOM_ATTRIBUTE_TEXT": "النص", "CUSTOM_ATTRIBUTE_TEXT": "النص",
@@ -52,28 +52,28 @@
"CUSTOM_ATTRIBUTE_LINK": "الرابط", "CUSTOM_ATTRIBUTE_LINK": "الرابط",
"CUSTOM_ATTRIBUTE_CHECKBOX": "مربع", "CUSTOM_ATTRIBUTE_CHECKBOX": "مربع",
"CREATED_AT": "تم إنشاؤها في", "CREATED_AT": "تم إنشاؤها في",
"LAST_ACTIVITY": "النشاط الأخير" "LAST_ACTIVITY": "آخر نشاط"
}, },
"GROUPS": { "GROUPS": {
"STANDARD_FILTERS": "Standard filters", "STANDARD_FILTERS": "الفلاتر القياسية",
"ADDITIONAL_FILTERS": "Additional filters", "ADDITIONAL_FILTERS": "فلاتر إضافية",
"CUSTOM_ATTRIBUTES": "Custom attributes" "CUSTOM_ATTRIBUTES": "سمات مخصصة"
}, },
"CUSTOM_VIEWS": { "CUSTOM_VIEWS": {
"ADD": { "ADD": {
"TITLE": "هل تريد حفظ هذا الفلتر؟", "TITLE": "هل تريد حفظ هذا الفلتر؟",
"LABEL": "تسمية هذا الفلتر", "LABEL": "تسمية هذا الفلتر",
"PLACEHOLDER": "Name your filter to refer it later.", "PLACEHOLDER": "أدخل اسم لهذا الفلتر",
"ERROR_MESSAGE": "الاسم مطلوب.", "ERROR_MESSAGE": "الاسم مطلوب",
"SAVE_BUTTON": "حفظ الفلتر", "SAVE_BUTTON": "حفظ الفلتر",
"CANCEL_BUTTON": "إلغاء", "CANCEL_BUTTON": "إلغاء",
"API_FOLDERS": { "API_FOLDERS": {
"SUCCESS_MESSAGE": "تم إنشاء طريقة عرض مخصصة بنجاح.", "SUCCESS_MESSAGE": "تم إنشاء طريقة عرض مخصصة بنجاح",
"ERROR_MESSAGE": "خطأ أثناء إنشاء طريقة عرض مخصصة." "ERROR_MESSAGE": "خطأ أثناء إنشاء طريقة عرض مخصصة"
}, },
"API_SEGMENTS": { "API_SEGMENTS": {
"SUCCESS_MESSAGE": "تم إنشاء طريقة عرض مخصصة بنجاح.", "SUCCESS_MESSAGE": "تم إنشاء طريقة عرض مخصصة بنجاح",
"ERROR_MESSAGE": "خطأ أثناء إنشاء طريقة عرض مخصصة." "ERROR_MESSAGE": "خطأ أثناء إنشاء طريقة عرض مخصصة"
} }
}, },
"EDIT": { "EDIT": {
@@ -85,17 +85,17 @@
"CONFIRM": { "CONFIRM": {
"TITLE": "تأكيد الحذف", "TITLE": "تأكيد الحذف",
"MESSAGE": "هل أنت متأكد من حذف الفلتر ", "MESSAGE": "هل أنت متأكد من حذف الفلتر ",
"YES": "Yes, delete", "YES": "نعم، احذف",
"NO": "لا، احتفظ به" "NO": "لا، احتفظ به"
} }
}, },
"API_FOLDERS": { "API_FOLDERS": {
"SUCCESS_MESSAGE": "تم حذف طريقة عرض مخصصة بنجاح.", "SUCCESS_MESSAGE": "تم حذف طريقة عرض مخصصة بنجاح",
"ERROR_MESSAGE": "حدث خطأ أثناء حذف المجلد." "ERROR_MESSAGE": "حدث خطأ أثناء حذف المجلد"
}, },
"API_SEGMENTS": { "API_SEGMENTS": {
"SUCCESS_MESSAGE": "تم حذف العرض المخصص بنجاح.", "SUCCESS_MESSAGE": "تم حذف العرض المخصص بنجاح",
"ERROR_MESSAGE": "حدث خطأ أثناء حذف طريقة عرض مخصصة." "ERROR_MESSAGE": "حدث خطأ أثناء حذف طريقة عرض مخصصة"
} }
} }
} }
@@ -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 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>", "SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": { "CSML_BOT_EDITOR": {
"NAME": { "NAME": {
"LABEL": "Bot name", "LABEL": "Bot Name",
"PLACEHOLDER": "Name your bot.", "PLACEHOLDER": "Give your bot a name",
"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": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.", "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.",
"SUBMIT": "تحديث", "SUBMIT": "تحديث",
"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.", "ERROR_MESSAGE": "Could not update the agent bot, please try again later",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.", "DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Select bot" "SELECT_PLACEHOLDER": "Select Bot"
}, },
"ADD": { "ADD": {
"TITLE": "Configure new bot", "TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "إلغاء", "CANCEL_BUTTON_TEXT": "إلغاء",
"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": "حذف", "BUTTON_TEXT": "حذف",
"TITLE": "Delete bot", "TITLE": "Delete Bot",
"SUBMIT": "حذف", "SUBMIT": "حذف",
"CANCEL_BUTTON_TEXT": "إلغاء", "CANCEL_BUTTON_TEXT": "إلغاء",
"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 delete bot. Please try again." "ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
} }
}, },
"EDIT": { "EDIT": {
"BUTTON_TEXT": "تعديل", "BUTTON_TEXT": "تعديل",
"LOADING": "Fetching bots...", "LOADING": "Fetching Bots...",
"TITLE": "Edit bot", "TITLE": "Edit Bot",
"CANCEL_BUTTON_TEXT": "إلغاء", "CANCEL_BUTTON_TEXT": "إلغاء",
"API": { "API": {
"SUCCESS_MESSAGE": "Bot updated successfully.", "SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot. Please try again." "ERROR_MESSAGE": "Could not update bot, Please try again later"
} }
}, },
"TYPES": { "TYPES": {
"WEBHOOK": "Webhook bot", "WEBHOOK": "Webhook Bot",
"CSML": "CSML bot" "CSML": "CSML Bot"
} }
} }
} }
@@ -7,34 +7,34 @@
"GO_BACK_LABEL": "العودة للخلف", "GO_BACK_LABEL": "العودة للخلف",
"ASSIGN_LABEL": "تكليف", "ASSIGN_LABEL": "تكليف",
"YES": "نعم", "YES": "نعم",
"ASSIGN_AGENT_TOOLTIP": "تعيين وكيل", "ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
"ASSIGN_TEAM_TOOLTIP": "تعيين فريق", "ASSIGN_TEAM_TOOLTIP": "تعيين فريق",
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح.", "ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
"ASSIGN_FAILED": "Failed to assign conversations. Please try again.", "ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح.", "RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
"RESOLVE_FAILED": "Failed to resolve conversations. Please try again.", "RESOLVE_FAILED": "فشل في حل المحادثات، يرجى المحاولة مرة أخرى",
"ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.", "ALL_CONVERSATIONS_SELECTED_ALERT": "المحادثات المرئية في هذه الصفحة هي المحددة فقط.",
"AGENT_LIST_LOADING": "جاري تحميل الوكلاء", "AGENT_LIST_LOADING": "تحميل الوكلاء",
"UPDATE": { "UPDATE": {
"CHANGE_STATUS": "تغيير الحالة", "CHANGE_STATUS": "تغيير الحالة",
"SNOOZE_UNTIL_NEXT_REPLY": "غفوة حتى الرد القادم.", "SNOOZE_UNTIL_NEXT_REPLY": "غفوة حتى الرد القادم",
"UPDATE_SUCCESFUL": "تم تحديث حالة المحادثة بنجاح.", "UPDATE_SUCCESFUL": "تم تحديث حالة المحادثة بنجاح.",
"UPDATE_FAILED": "Failed to update conversations. Please try again." "UPDATE_FAILED": "فشل تحديث المحادثات، الرجاء المحاولة مرة أخرى"
}, },
"LABELS": { "LABELS": {
"ASSIGN_LABELS": "Assign labels", "ASSIGN_LABELS": "تعيين التسميات",
"NO_LABELS_FOUND": "لم يتم العثور على تسميات لـ", "NO_LABELS_FOUND": "لم يتم العثور على تسميات لـ",
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة", "ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح.", "ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح",
"ASSIGN_FAILED": "Failed to assign labels. Please try again." "ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى"
}, },
"TEAMS": { "TEAMS": {
"TEAM_SELECT_LABEL": "اختيار فريق", "TEAM_SELECT_LABEL": "اختيار فريق",
"NONE": "لا شيء", "NONE": "لا شيء",
"NO_TEAMS_AVAILABLE": "لا توجد فرق مضافة إلى هذا الحساب حتى الآن.", "NO_TEAMS_AVAILABLE": "لا توجد فرق مضافة إلى هذا الحساب حتى الآن.",
"ASSIGN_SELECTED_TEAMS": "تعيين فريق محدد.", "ASSIGN_SELECTED_TEAMS": "تعيين فريق محدد",
"ASSIGN_SUCCESFUL": "تم تعيين الفرق بنجاح.", "ASSIGN_SUCCESFUL": "تم تعيين الفرق بنجاح",
"ASSIGN_FAILED": "Failed to assign team. Please try again." "ASSIGN_FAILED": "فشل تعيين الفريق، الرجاء المحاولة مرة أخرى"
} }
} }
} }
@@ -1,75 +1,75 @@
{ {
"CANNED_MGMT": { "CANNED_MGMT": {
"HEADER": "الردود السريعة", "HEADER": "الردود السريعة",
"HEADER_BTN_TXT": "Add canned response", "HEADER_BTN_TXT": "إضافة رد جاهز",
"LOADING": "Fetching canned responses...", "LOADING": "جاري جلب الردود الجاهزة",
"SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام.", "SEARCH_404": "لا توجد عناصر مطابقة لهذا الاستعلام",
"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>", "SIDEBAR_TXT": "<p><b>الردود الجاهزة</b> </p><p> الردود الجاهزة هي عبارة عن قوالب يتم إعدادها مسبقاً لاستخدامها في كتابة الرسائل والردود للإجابة على اكثر الاسئلة شيوعاً. </p><p> لإضافة قالب جديد، فقط انقر على <b>إضافة رد جاهز</b>. يمكنك أيضاً تعديل أو حذف الردود الجاهزة الأخرى بالنقر على زر التحرير أو الحذف </p><p> يتم تسهيل استخدام الردود المسبقة بواسطة <b>رموز مختصرة</b> لتسريع البحث والوصول إليها. يمكن للموظفين الوصول إلى قوالب الردود الجاهزة أثناء الدردشة عن طريق الضغط على زر <b>'/'</b> ومن ثم الاختصار الخاص بالقالب المراد إدخاله في مربع النص. </p>",
"LIST": { "LIST": {
"404": "لا توجد ردود جاهزة متوفرة في هذا الحساب.", "404": "لا توجد ردود جاهزة متوفرة في هذا الحساب.",
"TITLE": "إدارة الردود الجاهزة", "TITLE": "إدارة الردود الجاهزة",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.", "DESC": "الردود الجاهزة هي قوالب رسائل معدة مسبقاً يمكن استخدامها لتسريع كتابة الردود في المحادثات.",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Short code", "الرمز المختصر",
"المحتوى", "المحتوى",
"الإجراءات" "الإجراءات"
] ]
}, },
"ADD": { "ADD": {
"TITLE": "Add canned response", "TITLE": "إضافة رد جاهز",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.", "DESC": "الردود الجاهزة هي قوالب رسائل معدة مسبقاً يمكن استخدامها لتسريع كتابة الردود في المحادثات.",
"CANCEL_BUTTON_TEXT": "إلغاء", "CANCEL_BUTTON_TEXT": "إلغاء",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Short code", "LABEL": "الرمز المختصر",
"PLACEHOLDER": "Please enter a short code.", "PLACEHOLDER": "الرجاء إدخال رمز مختصر",
"ERROR": "Short Code is required." "ERROR": "الرمز المختصر مطلوب"
}, },
"CONTENT": { "CONTENT": {
"LABEL": "رسالة", "LABEL": "المحتوى",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.", "PLACEHOLDER": "الرجاء إدخال محتوى",
"ERROR": "Message is required." "ERROR": "المحتوى مطلوب"
}, },
"SUBMIT": "إرسال" "SUBMIT": "إرسال"
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response added successfully.", "SUCCESS_MESSAGE": "تم إضافة قالب الرد الجاهز بنجاح",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً" "ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
} }
}, },
"EDIT": { "EDIT": {
"TITLE": "Edit canned response", "TITLE": "تعديل قالب الرد الجاهز",
"CANCEL_BUTTON_TEXT": "إلغاء", "CANCEL_BUTTON_TEXT": "إلغاء",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Short code", "LABEL": "الرمز المختصر",
"PLACEHOLDER": "Please enter a shortcode.", "PLACEHOLDER": "الرجاء إدخال رمز مختصر",
"ERROR": "Short code is required." "ERROR": "الرمز المختصر مطلوب"
}, },
"CONTENT": { "CONTENT": {
"LABEL": "رسالة", "LABEL": "المحتوى",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.", "PLACEHOLDER": "الرجاء إدخال محتوى",
"ERROR": "الرسالة مطلوبة." "ERROR": "المحتوى مطلوب"
}, },
"SUBMIT": "إرسال" "SUBMIT": "إرسال"
}, },
"BUTTON_TEXT": "تعديل", "BUTTON_TEXT": "تعديل",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response is updated successfully.", "SUCCESS_MESSAGE": "تم تحديث قالب الرد الجاهز بنجاح",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً" "ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
} }
}, },
"DELETE": { "DELETE": {
"BUTTON_TEXT": "حذف", "BUTTON_TEXT": "حذف",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response deleted successfully.", "SUCCESS_MESSAGE": "تم حذف قالب الرد الجاهز بنجاح",
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً" "ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
}, },
"CONFIRM": { "CONFIRM": {
"TITLE": "تأكيد الحذف", "TITLE": "تأكيد الحذف",
"MESSAGE": "هل أنت متأكد من الحذف ", "MESSAGE": "هل أنت متأكد من الحذف ",
"YES": "Yes, delete ", "YES": "نعم، احذف ",
"NO": "No, keep " "NO": "لا، احتفظ "
} }
} }
} }
@@ -51,30 +51,18 @@
"ACTIVE": "النشاط الأخير" "ACTIVE": "النشاط الأخير"
} }
}, },
"SORT_ORDER_ITEMS": { "CHAT_SORT_FILTER_ITEMS": {
"last_activity_at_asc": { "latest": {
"TEXT": "Last activity: Oldest first" "TEXT": "النشاط الأخير"
}, },
"last_activity_at_desc": { "sort_on_created_at": {
"TEXT": "Last activity: Newest first" "TEXT": "تم إنشاؤها في"
}, },
"created_at_desc": { "sort_on_priority": {
"TEXT": "Created at: Newest first" "TEXT": "الأولوية"
}, },
"created_at_asc": { "sort_on_waiting_since": {
"TEXT": "Created at: Oldest first" "TEXT": "Pending Response"
},
"priority_desc": {
"TEXT": "Priority: Highest first"
},
"priority_asc": {
"TEXT": "Priority: Lowest first"
},
"waiting_since_asc": {
"TEXT": "Pending Response: Longest first"
},
"waiting_since_desc": {
"TEXT": "Pending Response: Shortest first"
} }
}, },
"ATTACHMENTS": { "ATTACHMENTS": {
@@ -41,9 +41,6 @@
"SAVE_CONTACT": "حفظ", "SAVE_CONTACT": "حفظ",
"UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...", "UPLOADING_ATTACHMENTS": "جاري تحميل المرفقات...",
"REPLIED_TO_STORY": "Replied to your story", "REPLIED_TO_STORY": "Replied to your story",
"UNSUPPORTED_MESSAGE": "This message is unsupported.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
"SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح", "SUCCESS_DELETE_MESSAGE": "تم حذف الرسالة بنجاح",
"FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى", "FAIL_DELETE_MESSSAGE": "تعذر حذف الرسالة! حاول مرة أخرى",
"NO_RESPONSE": "لا توجد استجابة", "NO_RESPONSE": "لا توجد استجابة",
@@ -142,7 +139,6 @@
"PRIVATE_NOTE": "إضافة ملاحظة خاصة", "PRIVATE_NOTE": "إضافة ملاحظة خاصة",
"SEND": "إرسال", "SEND": "إرسال",
"CREATE": "إضافة ملاحظة", "CREATE": "إضافة ملاحظة",
"INSERT_READ_MORE": "Read more",
"DISMISS_REPLY": "Dismiss reply", "DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:", "REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "عرض محرر النصوص", "TIP_FORMAT_ICON": "عرض محرر النصوص",
@@ -92,7 +92,7 @@
}, },
"NETWORK": { "NETWORK": {
"NOTIFICATION": { "NOTIFICATION": {
"OFFLINE": "غير متصل" "TEXT": "قطع الاتصال من Chatwoot"
}, },
"BUTTON": { "BUTTON": {
"REFRESH": "تحديث" "REFRESH": "تحديث"
@@ -3,7 +3,6 @@
"HEADER": { "HEADER": {
"FILTER": "تصفية حسب", "FILTER": "تصفية حسب",
"SORT": "ترتيب حسب", "SORT": "ترتيب حسب",
"LOCALE": "اللغة",
"SETTINGS_BUTTON": "الإعدادات", "SETTINGS_BUTTON": "الإعدادات",
"NEW_BUTTON": "مقالة جديدة", "NEW_BUTTON": "مقالة جديدة",
"DROPDOWN_OPTIONS": { "DROPDOWN_OPTIONS": {
@@ -16,12 +15,6 @@
"MINE": "مقالاتي", "MINE": "مقالاتي",
"DRAFT": "مقالات مسودة", "DRAFT": "مقالات مسودة",
"ARCHIVED": "المقالات المؤرشفة" "ARCHIVED": "المقالات المؤرشفة"
},
"LOCALE_SELECT": {
"TITLE": "Select locale",
"PLACEHOLDER": "Select locale",
"NO_RESULT": "No locale found",
"SEARCH_PLACEHOLDER": "Search locale"
} }
}, },
"EDIT_HEADER": { "EDIT_HEADER": {
@@ -83,9 +76,6 @@
}, },
"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": "يبحث...",
"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",
@@ -230,10 +220,7 @@
"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": "الاسم", "LABEL": "الاسم",
@@ -439,21 +426,6 @@
} }
} }
}, },
"ARTICLE_SEARCH": {
"TITLE": "Search articles",
"PLACEHOLDER": "Search articles",
"NO_RESULT": "No articles found",
"SEARCHING": "يبحث...",
"SEARCH_BUTTON": "بحث",
"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": "أغلق",
"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.",
@@ -1,18 +1,18 @@
{ {
"FILTER": { "FILTER": {
"TITLE": "Филтрирай разговорите", "TITLE": "Филтрирай разговорите",
"SUBTITLE": "Add your filters below and hit 'Apply filters' to cut through the chat clutter.", "SUBTITLE": "Добавете филтри по-долу и натиснете „Прилагане на филтри“, за да филтрирате разговорите.",
"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": "Добавяне на филтър",
"FILTER_DELETE_ERROR": "Oops, looks like we can't save nothing! Please add at least one filter to save it.", "FILTER_DELETE_ERROR": "За да запазите, трябва да имате поне един филтър",
"SUBMIT_BUTTON_LABEL": "Прилагане на филтри", "SUBMIT_BUTTON_LABEL": "Прилагане на филтри",
"UPDATE_BUTTON_LABEL": "Update folder", "UPDATE_BUTTON_LABEL": "Update folder",
"CANCEL_BUTTON_LABEL": "Отмени", "CANCEL_BUTTON_LABEL": "Отмени",
"CLEAR_BUTTON_LABEL": "Clear filters", "CLEAR_BUTTON_LABEL": "Изчисти филтрите",
"EMPTY_VALUE_ERROR": "Изисква се стойност",
"FOLDER_LABEL": "Folder Name", "FOLDER_LABEL": "Folder Name",
"FOLDER_QUERY_LABEL": "Folder Query", "FOLDER_QUERY_LABEL": "Folder Query",
"EMPTY_VALUE_ERROR": "Изисква се стойност.",
"TOOLTIP_LABEL": "Филтрирай разговорите", "TOOLTIP_LABEL": "Филтрирай разговорите",
"QUERY_DROPDOWN_LABELS": { "QUERY_DROPDOWN_LABELS": {
"AND": "И", "AND": "И",
@@ -36,44 +36,44 @@
}, },
"ATTRIBUTES": { "ATTRIBUTES": {
"STATUS": "Статус", "STATUS": "Статус",
"ASSIGNEE_NAME": "Assignee name", "ASSIGNEE_NAME": "Име на правоприемника",
"INBOX_NAME": "Inbox name", "INBOX_NAME": "Име на входящата кутия",
"TEAM_NAME": "Team name", "TEAM_NAME": "Име на екипа",
"CONVERSATION_IDENTIFIER": "Conversation identifier", "CONVERSATION_IDENTIFIER": "Идентивикатор на разговор",
"CAMPAIGN_NAME": "Campaign name", "CAMPAIGN_NAME": "Име на кампания",
"LABELS": "Етикети", "LABELS": "Етикети",
"BROWSER_LANGUAGE": "Browser language", "BROWSER_LANGUAGE": "Език на браузъра",
"PRIORITY": "Priority", "PRIORITY": "Priority",
"COUNTRY_NAME": "Country name", "COUNTRY_NAME": "Име на държавата",
"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",
"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_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": "Name your filter to refer it later.", "PLACEHOLDER": "Enter a name for this filter",
"ERROR_MESSAGE": "Name is required.", "ERROR_MESSAGE": "Name is required",
"SAVE_BUTTON": "Save filter", "SAVE_BUTTON": "Save filter",
"CANCEL_BUTTON": "Отмени", "CANCEL_BUTTON": "Отмени",
"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": {
@@ -85,17 +85,17 @@
"CONFIRM": { "CONFIRM": {
"TITLE": "Потвърди изтриването", "TITLE": "Потвърди изтриването",
"MESSAGE": "Are you sure to delete the filter ", "MESSAGE": "Are you sure to delete the filter ",
"YES": "Yes, delete", "YES": "Да, изтрий",
"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"
} }
} }
} }
@@ -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 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>", "SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": { "CSML_BOT_EDITOR": {
"NAME": { "NAME": {
"LABEL": "Bot name", "LABEL": "Bot Name",
"PLACEHOLDER": "Name your bot.", "PLACEHOLDER": "Give your bot a name",
"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": "Assign an Agent Bot to your inbox. They can handle initial conversations and transfer them to a live agent when necessary.", "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.",
"SUBMIT": "Обновяване", "SUBMIT": "Обновяване",
"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.", "ERROR_MESSAGE": "Could not update the agent bot, please try again later",
"DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot. Please try again.", "DISCONNECTED_ERROR_MESSAGE": "Could not disconnect the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Select bot" "SELECT_PLACEHOLDER": "Select Bot"
}, },
"ADD": { "ADD": {
"TITLE": "Configure new bot", "TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "Отмени", "CANCEL_BUTTON_TEXT": "Отмени",
"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": "Изтрий", "BUTTON_TEXT": "Изтрий",
"TITLE": "Delete bot", "TITLE": "Delete Bot",
"SUBMIT": "Изтрий", "SUBMIT": "Изтрий",
"CANCEL_BUTTON_TEXT": "Отмени", "CANCEL_BUTTON_TEXT": "Отмени",
"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 delete bot. Please try again." "ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
} }
}, },
"EDIT": { "EDIT": {
"BUTTON_TEXT": "Редактирай", "BUTTON_TEXT": "Редактирай",
"LOADING": "Fetching bots...", "LOADING": "Fetching Bots...",
"TITLE": "Edit bot", "TITLE": "Edit Bot",
"CANCEL_BUTTON_TEXT": "Отмени", "CANCEL_BUTTON_TEXT": "Отмени",
"API": { "API": {
"SUCCESS_MESSAGE": "Bot updated successfully.", "SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot. Please try again." "ERROR_MESSAGE": "Could not update bot, Please try again later"
} }
}, },
"TYPES": { "TYPES": {
"WEBHOOK": "Webhook bot", "WEBHOOK": "Webhook Bot",
"CSML": "CSML bot" "CSML": "CSML Bot"
} }
} }
} }
@@ -1,40 +1,40 @@
{ {
"BULK_ACTION": { "BULK_ACTION": {
"CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected", "CONVERSATIONS_SELECTED": "%{conversationCount} conversations selected",
"AGENT_SELECT_LABEL": "Изберете агент", "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": "Изберете екип", "TEAM_SELECT_LABEL": "Select Team",
"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"
} }
} }
} }
@@ -1,75 +1,75 @@
{ {
"CANNED_MGMT": { "CANNED_MGMT": {
"HEADER": "Готови отговори", "HEADER": "Готови отговори",
"HEADER_BTN_TXT": "Add canned response", "HEADER_BTN_TXT": "Добавяне на готов отговор",
"LOADING": "Fetching canned responses...", "LOADING": "Извличане на готови отговори",
"SEARCH_404": "Няма резултати отговарящи на тази заявка.", "SEARCH_404": "Няма резултати отговарящи на тази заявка",
"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>", "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>",
"LIST": { "LIST": {
"404": "Няма налични готови отговори в този акаунт.", "404": "Няма налични готови отговори в този акаунт.",
"TITLE": "Управлявайте готовите отговори", "TITLE": "Управлявайте готовите отговори",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.", "DESC": "Готовите отговори са предварително дефинирани шаблони за отговор, които могат да се изпращат бързо в чата.",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Short code", "Кратък код",
"Съдържание", "Съдържание",
"Действия" "Действия"
] ]
}, },
"ADD": { "ADD": {
"TITLE": "Add canned response", "TITLE": "Добавяне на готов отговор",
"DESC": "Canned Responses are predefined reply templates which can be used to quickly send out replies to conversations.", "DESC": "Canned Responses are saved reply templates which can be used to quickly send out reply to conversation.",
"CANCEL_BUTTON_TEXT": "Отмени", "CANCEL_BUTTON_TEXT": "Отмени",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Short code", "LABEL": "Кратък код",
"PLACEHOLDER": "Please enter a short code.", "PLACEHOLDER": "Please enter a short code",
"ERROR": "Short Code is required." "ERROR": "Краткия код е задължителен"
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Съобщение", "LABEL": "Съдържание",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.", "PLACEHOLDER": "Моля, въведете съдържание",
"ERROR": "Message is required." "ERROR": "Съдържанието е задължително"
}, },
"SUBMIT": "Изпращане" "SUBMIT": "Изпращане"
}, },
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response added successfully.", "SUCCESS_MESSAGE": "Готовия отговор е добавен успешно",
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно" "ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
} }
}, },
"EDIT": { "EDIT": {
"TITLE": "Edit canned response", "TITLE": "Промени готовия отговор",
"CANCEL_BUTTON_TEXT": "Отмени", "CANCEL_BUTTON_TEXT": "Отмени",
"FORM": { "FORM": {
"SHORT_CODE": { "SHORT_CODE": {
"LABEL": "Short code", "LABEL": "Кратък код",
"PLACEHOLDER": "Please enter a shortcode.", "PLACEHOLDER": "Моля, въведете кратък код",
"ERROR": "Short code is required." "ERROR": "Краткия код е задължителен"
}, },
"CONTENT": { "CONTENT": {
"LABEL": "Съобщение", "LABEL": "Съдържание",
"PLACEHOLDER": "Please write the message you want to save as a template to use later.", "PLACEHOLDER": "Моля, въведете съдържание",
"ERROR": "Message is required." "ERROR": "Съдържанието е задължително"
}, },
"SUBMIT": "Изпращане" "SUBMIT": "Изпращане"
}, },
"BUTTON_TEXT": "Редактирай", "BUTTON_TEXT": "Редактирай",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response is updated successfully.", "SUCCESS_MESSAGE": "Готовия отговор е обновен успешно",
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно" "ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
} }
}, },
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Изтрий", "BUTTON_TEXT": "Изтрий",
"API": { "API": {
"SUCCESS_MESSAGE": "Canned response deleted successfully.", "SUCCESS_MESSAGE": "Готовия отговор е изтрит успешно",
"ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно" "ERROR_MESSAGE": "Не можа да се свърже с Woot сървър. Моля, опитайте отново по-късно"
}, },
"CONFIRM": { "CONFIRM": {
"TITLE": "Потвърди изтриването", "TITLE": "Потвърди изтриването",
"MESSAGE": "Сигурни ли сте за изтриването ", "MESSAGE": "Сигурни ли сте за изтриването ",
"YES": "Yes, delete ", "YES": "Да, изтрий ",
"NO": "No, keep " "NO": "Не, запази "
} }
} }
} }
@@ -51,30 +51,18 @@
"ACTIVE": "Last activity" "ACTIVE": "Last activity"
} }
}, },
"SORT_ORDER_ITEMS": { "CHAT_SORT_FILTER_ITEMS": {
"last_activity_at_asc": { "latest": {
"TEXT": "Last activity: Oldest first" "TEXT": "Last activity"
}, },
"last_activity_at_desc": { "sort_on_created_at": {
"TEXT": "Last activity: Newest first" "TEXT": "Създаден в"
}, },
"created_at_desc": { "sort_on_priority": {
"TEXT": "Created at: Newest first" "TEXT": "Priority"
}, },
"created_at_asc": { "sort_on_waiting_since": {
"TEXT": "Created at: Oldest first" "TEXT": "Pending Response"
},
"priority_desc": {
"TEXT": "Priority: Highest first"
},
"priority_asc": {
"TEXT": "Priority: Lowest first"
},
"waiting_since_asc": {
"TEXT": "Pending Response: Longest first"
},
"waiting_since_desc": {
"TEXT": "Pending Response: Shortest first"
} }
}, },
"ATTACHMENTS": { "ATTACHMENTS": {
@@ -41,9 +41,6 @@
"SAVE_CONTACT": "Save", "SAVE_CONTACT": "Save",
"UPLOADING_ATTACHMENTS": "Uploading attachments...", "UPLOADING_ATTACHMENTS": "Uploading attachments...",
"REPLIED_TO_STORY": "Replied to your story", "REPLIED_TO_STORY": "Replied to your story",
"UNSUPPORTED_MESSAGE": "This message is unsupported.",
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully", "SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again", "FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
"NO_RESPONSE": "No response", "NO_RESPONSE": "No response",
@@ -142,7 +139,6 @@
"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",
@@ -92,7 +92,7 @@
}, },
"NETWORK": { "NETWORK": {
"NOTIFICATION": { "NOTIFICATION": {
"OFFLINE": "Offline" "TEXT": "Disconnected from Chatwoot"
}, },
"BUTTON": { "BUTTON": {
"REFRESH": "Refresh" "REFRESH": "Refresh"

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