Compare commits

..
Author SHA1 Message Date
Shivam Mishra be29211f5b feat: add navigate option 2023-10-11 12:46:40 +05:30
Shivam Mishra 55918c0bde feat: add id for message 2023-10-11 12:43:16 +05:30
Shivam Mishra 4d2e43436b feat: show reply to for agent message 2023-10-11 12:38:10 +05:30
Shivam Mishra 243188b9fe feat: add arrow reply icon 2023-10-11 12:37:55 +05:30
Shivam Mishra acae88b27a fix: darkmode color 2023-10-10 19:33:37 +05:30
Shivam Mishra cadd701d74 fix: colors on reply to 2023-10-10 19:32:14 +05:30
Shivam Mishra 6985dacc34 refactor: externalize the style 2023-10-10 19:31:03 +05:30
Shivam Mishra db5a5f1a03 fix: syntax 2023-10-10 19:30:35 +05:30
Shivam Mishra f7221df333 refactor: use MessagePreview in reply to 2023-10-10 19:30:01 +05:30
Shivam Mishra e6896dc359 refactor: allow custom empty message 2023-10-10 19:29:37 +05:30
Shivam Mishra 52f139561e feat: hide attachment icon 2023-10-10 19:22:11 +05:30
Shivam Mishra f509a734f9 refactor: use message preview in reply-to-message 2023-10-10 19:22:11 +05:30
Shivam Mishra e61f5a4e62 refactor: styles for message preview 2023-10-10 19:21:46 +05:30
Shivam Mishra 33cdd2c0ed refactor: split message-preview out as a component 2023-10-10 19:21:45 +05:30
Shivam Mishra a607bf4b60 refactor: visually balance y-padding 2023-10-10 19:21:45 +05:30
Shivam Mishra 7added2d48 feat: support attachments in bubble 2023-10-10 19:21:45 +05:30
Shivam Mishra d3dd20e4b9 fix: prop value 2023-10-10 19:21:45 +05:30
Shivam Mishra 8db377d29b feat: allow translation 2023-10-10 19:21:39 +05:30
Shivam Mishra 603c45a907 refactor: use vue 2 syntax 2023-10-10 19:21:38 +05:30
Shivam Mishra 1dcaec1186 feat: handle attachments in ReplyToMessage 2023-10-10 19:20:29 +05:30
Shivam Mishra d26959f57c feat: include attachments in props 2023-10-10 19:19:01 +05:30
Shivam Mishra c114cc7fa0 refactor: move ATTACHMENT_ICONS to constants 2023-10-10 19:18:34 +05:30
Shivam Mishra 7abe5b01dc fix: condition 2023-10-10 19:18:34 +05:30
Shivam Mishra 047f1f6bc3 refactor: handle translation externally 2023-10-10 19:18:34 +05:30
Shivam Mishra 86ea2bbbe3 refactor: handle missing message content 2023-10-10 19:18:34 +05:30
Shivam Mishra 53ac68f0c0 refactor: disable reply to bubble 2023-10-10 19:18:33 +05:30
Shivam Mishra a2af5e25e1 feat: allow showing in-reply-to message 2023-10-10 19:18:33 +05:30
711 changed files with 3440 additions and 10551 deletions
+1 -20
View File
@@ -81,7 +81,7 @@ SMTP_OPENSSL_VERIFY_MODE=peer
# Mail Incoming
# This is the domain set for the reply emails when conversation continuity is enabled
MAILER_INBOUND_EMAIL_DOMAIN=
# Set this to the appropriate ingress channel with regards to incoming emails
# Set this to appropriate ingress channel with regards to incoming emails
# Possible values are :
# relay for Exim, Postfix, Qmail
# mailgun for Mailgun
@@ -91,20 +91,10 @@ MAILER_INBOUND_EMAIL_DOMAIN=
RAILS_INBOUND_EMAIL_SERVICE=
# Use one of the following based on the email ingress service
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
# Set this to a password of your choice and use it in the Inbound webhook
RAILS_INBOUND_EMAIL_PASSWORD=
MAILGUN_INGRESS_SIGNING_KEY=
MANDRILL_INGRESS_API_KEY=
# Creating Your Inbound Webhook Instructions for Postmark and Sendgrid:
# Inbound webhook URL format:
# https://actionmailbox:[YOUR_RAILS_INBOUND_EMAIL_PASSWORD]@[YOUR_CHATWOOT_DOMAIN.COM]/rails/action_mailbox/[RAILS_INBOUND_EMAIL_SERVICE]/inbound_emails
# Note: Replace the values inside the brackets; do not include the brackets themselves.
# Example: https://actionmailbox:mYRandomPassword3@chatwoot.example.com/rails/action_mailbox/postmark/inbound_emails
# For Postmark
# Ensure the 'Include raw email content in JSON payload' checkbox is selected in the inbound webhook section.
# Storage
ACTIVE_STORAGE_SERVICE=local
@@ -186,9 +176,6 @@ ANDROID_SHA256_CERT_FINGERPRINT=AC:73:8E:DE:EB:56:EA:CC:10:87:02:A7:65:37:7B:38:
# MICROSOFT CLARITY
# MS_CLARITY_TOKEN=xxxxxxxxx
# GOOGLE_TAG_MANAGER
# GOOGLE_TAG = GTM-XXXXXXX
## Scout
## https://scoutapm.com/docs/ruby/configuration
# SCOUT_KEY=YOURKEY
@@ -253,9 +240,3 @@ AZURE_APP_SECRET=
# Sentiment analysis model file path
SENTIMENT_FILE_PATH=
# Housekeeping/Performance related configurations
# Set to true if you want to remove stale contact inboxes
# contact_inboxes with no conversation older than 90 days will be removed
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
-1
View File
@@ -4,7 +4,6 @@ module.exports = {
'prettier',
'plugin:vue/recommended',
'plugin:storybook/recommended',
'plugin:cypress/recommended',
],
parserOptions: {
parser: '@babel/eslint-parser',
+8 -10
View File
@@ -15,7 +15,7 @@ gem 'browser'
gem 'hashie'
gem 'jbuilder'
gem 'kaminari'
gem 'responders', '>= 3.1.1'
gem 'responders'
gem 'rest-client'
gem 'telephone_number'
gem 'time_diff'
@@ -67,7 +67,7 @@ gem 'webpacker'
gem 'barnes'
##--- gems for authentication & authorization ---##
gem 'devise', '>= 4.9.3'
gem 'devise'
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
gem 'devise_token_auth'
# authorization
@@ -107,16 +107,16 @@ gem 'google-cloud-translate-v3'
gem 'ddtrace', require: false
gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', '>= 1.6.2', require: false
gem 'newrelic-sidekiq-metrics', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.13.0', require: false
gem 'sentry-rails', '>= 5.11.0', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.13.0', require: false
gem 'sentry-sidekiq', '>= 5.11.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.1.3'
# We want cron jobs
gem 'sidekiq-cron', '>= 1.11.0'
gem 'sidekiq-cron', '>= 1.10.1'
##-- Push notification service --##
gem 'fcm'
@@ -154,12 +154,12 @@ gem 'stripe'
gem 'faker'
# Include logrange conditionally in intializer using env variable
gem 'lograge', '~> 0.14.0', require: false
gem 'lograge', '~> 0.13.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
gem 'audited', '~> 5.4', '>= 5.4.1'
gem 'audited', '~> 5.4', '>= 5.4.0'
# need for google auth
gem 'omniauth'
@@ -200,8 +200,6 @@ group :development do
# profiling
gem 'rack-mini-profiler', '>= 3.1.1', require: false
gem 'stackprof'
# Should install the associated chrome extension to view query logs
gem 'meta_request'
end
group :test do
+34 -42
View File
@@ -126,9 +126,9 @@ GEM
rake (>= 10.4, < 14.0)
ast (2.4.2)
attr_extras (7.1.0)
audited (5.4.1)
activerecord (>= 5.0, < 7.7)
activesupport (>= 5.0, < 7.7)
audited (5.4.0)
activerecord (>= 5.0, < 7.2)
request_store (~> 1.2)
aws-eventstream (1.2.0)
aws-partitions (1.760.0)
aws-sdk-core (3.171.1)
@@ -148,8 +148,7 @@ GEM
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
base64 (0.1.1)
bcrypt (3.1.19)
bcrypt (3.1.18)
bindex (0.8.1)
blingfire (0.1.8)
bootsnap (1.16.0)
@@ -175,7 +174,7 @@ GEM
rexml
crass (1.0.6)
csv-safe (3.2.1)
cypress-on-rails (1.16.0)
cypress-on-rails (1.13.1)
rack
database_cleaner (2.0.2)
database_cleaner-active_record (>= 2, < 3)
@@ -194,7 +193,7 @@ GEM
irb (>= 1.5.0)
reline (>= 0.3.1)
declarative (0.0.20)
devise (4.9.3)
devise (4.9.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -262,7 +261,7 @@ GEM
rake
flag_shih_tzu (0.3.23)
foreman (0.87.2)
fugit (1.9.0)
fugit (1.8.1)
et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4)
gapic-common (0.18.0)
@@ -435,12 +434,12 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.14.0)
lograge (0.13.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.21.4)
loofah (2.21.3)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -451,16 +450,13 @@ GEM
marcel (1.0.2)
maxminddb (0.1.22)
memoist (0.16.2)
meta_request (0.7.4)
rack-contrib (>= 1.1, < 3)
railties (>= 3.0.0, < 7.1)
method_source (1.0.0)
mime-types (3.4.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2023.0218.1)
mini_magick (4.12.0)
mini_mime (1.1.5)
mini_portile2 (2.8.5)
mini_portile2 (2.8.4)
minitest (5.20.0)
mock_redis (0.36.0)
ruby2_keywords
@@ -482,11 +478,10 @@ GEM
net-smtp (0.3.3)
net-protocol
netrc (0.11.0)
newrelic-sidekiq-metrics (1.6.2)
newrelic_rpm (>= 8.0.0)
newrelic-sidekiq-metrics (1.6.1)
newrelic_rpm (~> 8)
sidekiq
newrelic_rpm (9.6.0)
base64
newrelic_rpm (8.16.0)
nio4r (2.5.9)
nokogiri (1.15.4)
mini_portile2 (~> 2.8.2)
@@ -558,12 +553,10 @@ GEM
pundit (2.3.0)
activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.7.3)
racc (1.7.1)
rack (2.2.8)
rack-attack (6.7.0)
rack (>= 1.0, < 4)
rack-contrib (2.4.0)
rack (< 4)
rack-cors (2.0.1)
rack (>= 2.0.0)
rack-mini-profiler (3.1.1)
@@ -604,13 +597,13 @@ GEM
thor (~> 1.0)
zeitwerk (~> 2.5)
rainbow (3.1.1)
rake (13.1.0)
rake (13.0.6)
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
redis (5.0.6)
redis-client (>= 0.9.0)
redis-client (0.18.0)
redis-client (0.17.0)
connection_pool
redis-namespace (1.10.0)
redis (>= 4)
@@ -623,7 +616,7 @@ GEM
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.1)
responders (3.1.0)
actionpack (>= 5.2)
railties (>= 5.2)
rest-client (2.1.0)
@@ -709,23 +702,23 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.13.0)
sentry-rails (5.11.0)
railties (>= 5.0)
sentry-ruby (~> 5.13.0)
sentry-ruby (5.13.0)
sentry-ruby (~> 5.11.0)
sentry-ruby (5.11.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.13.0)
sentry-ruby (~> 5.13.0)
sentry-sidekiq (5.11.0)
sentry-ruby (~> 5.11.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.2.0)
sidekiq (7.1.3)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
redis-client (>= 0.14.0)
sidekiq-cron (1.11.0)
sidekiq-cron (1.10.1)
fugit (~> 1.8)
globalid (>= 1.0.1)
sidekiq (>= 6)
@@ -765,7 +758,7 @@ GEM
stripe (8.5.0)
telephone_number (1.4.20)
test-prof (1.2.1)
thor (1.3.0)
thor (1.2.2)
tilt (2.2.0)
time_diff (0.3.0)
activesupport
@@ -823,7 +816,7 @@ GEM
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.12)
zeitwerk (2.6.11)
PLATFORMS
arm64-darwin-20
@@ -845,7 +838,7 @@ DEPENDENCIES
administrate-field-belongs_to_search
annotate
attr_extras
audited (~> 5.4, >= 5.4.1)
audited (~> 5.4, >= 5.4.0)
aws-sdk-s3
azure-storage-blob!
barnes
@@ -862,7 +855,7 @@ DEPENDENCIES
database_cleaner
ddtrace
debug (~> 1.8)
devise (>= 4.9.3)
devise
devise-secure_password!
devise_token_auth
dotenv-rails
@@ -898,12 +891,11 @@ DEPENDENCIES
line-bot-api
liquid
listen
lograge (~> 0.14.0)
lograge (~> 0.13.0)
maxminddb
meta_request
mock_redis
neighbor
newrelic-sidekiq-metrics (>= 1.6.2)
newrelic-sidekiq-metrics
newrelic_rpm
omniauth
omniauth-google-oauth2
@@ -923,7 +915,7 @@ DEPENDENCIES
rails (~> 7.0.8.0)
redis
redis-namespace
responders (>= 3.1.1)
responders
rest-client
reverse_markdown
rspec-rails
@@ -935,12 +927,12 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.13.0)
sentry-rails (>= 5.11.0)
sentry-ruby
sentry-sidekiq (>= 5.13.0)
sentry-sidekiq (>= 5.11.0)
shoulda-matchers
sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.11.0)
sidekiq-cron (>= 1.10.1)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.2.0)
spring
+2 -2
View File
@@ -1,3 +1,3 @@
backend: RAILS_ENV=test bin/rails s -p 5050
frontend: export NODE_OPTIONS=--openssl-legacy-provider && bin/webpack-dev-server
worker: RAILS_ENV=test dotenv bundle exec sidekiq -C config/sidekiq.yml
frontend: bin/webpack-dev-server
worker: dotenv RAILS_ENV=test bundle exec sidekiq -C config/sidekiq.yml
+11 -10
View File
@@ -1,12 +1,10 @@
<img src="https://user-images.githubusercontent.com/2246121/282256557-1570674b-d142-4198-9740-69404cc6a339.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282256632-87f6a01b-6467-4e0e-8a93-7bbf66d03a17.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
<p align="center">
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240" />
___
<p align="center">Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.</p>
</p>
# Chatwoot
Customer engagement suite, an open-source alternative to Intercom, Zendesk, Salesforce Service Cloud etc.
<p>
<p align="center">
<a href="https://heroku.com/deploy?template=https://github.com/chatwoot/chatwoot/tree/master" alt="Deploy to Heroku">
<img width="150" alt="Deploy" src="https://www.herokucdn.com/deploy/button.svg"/>
</a>
@@ -15,7 +13,9 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
</a>
</p>
<p>
___
<p align="center">
<a href="https://codeclimate.com/github/chatwoot/chatwoot/maintainability"><img src="https://api.codeclimate.com/v1/badges/e6e3f66332c91e5a4c0c/maintainability" alt="Maintainability"></a>
<img src="https://img.shields.io/circleci/build/github/chatwoot/chatwoot" alt="CircleCI Badge">
<a href="https://hub.docker.com/r/chatwoot/chatwoot/"><img src="https://img.shields.io/docker/pulls/chatwoot/chatwoot" alt="Docker Pull Badge"></a>
@@ -29,8 +29,9 @@ Customer engagement suite, an open-source alternative to Intercom, Zendesk, Sale
<a href="https://artifacthub.io/packages/helm/chatwoot/chatwoot"><img src="https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/artifact-hub" alt="Artifact HUB"></a>
</p>
<img src="https://user-images.githubusercontent.com/2246121/282255783-ee8a50c9-f42d-4752-8201-2d59965a663d.png#gh-light-mode-only" width="100%" alt="Chat dashboard dark mode"/>
<img src="https://user-images.githubusercontent.com/2246121/282255784-3d1994ec-d895-4ff5-ac68-d819987e1869.png#gh-dark-mode-only" width="100%" alt="Chat dashboard"/>
<img src="https://chatwoot-public-assets.s3.amazonaws.com/github/screenshot.png" width="100%" alt="Chat dashboard"/>
Chatwoot is an open-source, self-hosted customer engagement suite. Chatwoot lets you view and manage your customer data, communicate with them irrespective of which medium they use, and re-engage them based on their profile.
+1 -1
View File
@@ -1 +1 @@
2.6.0
2.5.0
@@ -93,9 +93,6 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: @message_type,
content: response.content,
source_id: response.identifier,
content_attributes: {
in_reply_to_external_id: response.in_reply_to_external_id
},
sender: @outgoing_echo ? nil : @contact_inbox.contact
}
end
@@ -76,10 +76,6 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message[:reply_to][:story] if message[:reply_to].present? && message[:reply_to][:story].present?
end
def message_reply_attributes
message[:reply_to][:mid] if message[:reply_to].present? && message[:reply_to][:mid].present?
end
def build_message
return if @outgoing_echo && already_sent_from_chatwoot?
return if message_content.blank? && all_unsupported_files?
@@ -122,10 +118,7 @@ class Messages::Instagram::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: message_type,
source_id: message_identifier,
content: message_content,
sender: @outgoing_echo ? nil : contact,
content_attributes: {
in_reply_to_external_id: message_reply_attributes
}
sender: @outgoing_echo ? nil : contact
}
end
+3 -35
View File
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
@user = user
@message_type = params[:message_type] || 'outgoing'
@attachments = params[:attachments]
@automation_rule = content_attributes&.dig(:automation_rule_id)
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
return unless params.instance_of?(ActionController::Parameters)
@in_reply_to = content_attributes&.dig(:in_reply_to)
@items = content_attributes&.dig(:items)
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
end
def perform
@@ -26,38 +26,6 @@ class Messages::MessageBuilder
private
# Extracts content attributes from the given params.
# - Converts ActionController::Parameters to a regular hash if needed.
# - Attempts to parse a JSON string if content is a string.
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
def content_attributes
params = convert_to_hash(@params)
content_attributes = params.fetch(:content_attributes, {})
return parse_json(content_attributes) if content_attributes.is_a?(String)
return content_attributes if content_attributes.is_a?(Hash)
{}
end
# Converts the given object to a hash.
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
# Otherwise, returns the object as-is.
def convert_to_hash(obj)
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
obj
end
# Attempts to parse a string as JSON.
# If successful, returns the parsed hash with symbolized names.
# If unsuccessful, returns nil.
def parse_json(content)
JSON.parse(content, symbolize_names: true)
rescue JSON::ParserError
{}
end
def process_attachments
return if @attachments.blank?
+1 -14
View File
@@ -15,10 +15,7 @@ class V2::ReportBuilder
end
def timeseries
return send(params[:metric]) if metric_valid?
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
{}
send(params[:metric])
end
# For backward compatible with old report
@@ -56,16 +53,6 @@ class V2::ReportBuilder
private
def metric_valid?
%w[conversations_count
incoming_messages_count
outgoing_messages_count
avg_first_response_time
avg_resolution_time reply_time
resolutions_count
reply_time].include?(params[:metric])
end
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
@@ -10,7 +10,7 @@ class Api::V1::Accounts::ArticlesController < Api::V1::Accounts::BaseController
@articles_count = @all_articles.count
@articles = if list_params[:category_slug].present?
@all_articles.order_by_position.page(@current_page)
@all_articles.order_by_position.page(@current_page).per(50)
else
@all_articles.order_by_updated_at.page(@current_page)
end
@@ -48,7 +48,7 @@ class Api::V1::Accounts::CategoriesController < Api::V1::Accounts::BaseControlle
def category_params
params.require(:category).permit(
:name, :description, :position, :slug, :locale, :icon, :parent_category_id, :associated_category_id
:name, :description, :position, :slug, :locale, :parent_category_id, :associated_category_id
)
end
@@ -60,7 +60,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def toggle_status
if params[:status].present?
if params[:status]
set_conversation_status
@status = @conversation.save!
else
@@ -47,11 +47,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
head :ok
end
def logo
@portal.logo.purge if @portal.logo.attached?
head :ok
end
def process_attached_logo
blob_id = params[:blob_id]
blob = ActiveStorage::Blob.find_by(id: blob_id)
@@ -79,9 +79,6 @@ class Api::V1::Widget::BaseController < ApplicationController
sender: @contact,
content: permitted_params[:message][:content],
inbox_id: conversation.inbox_id,
content_attributes: {
in_reply_to: permitted_params[:message][:reply_to]
},
echo_id: permitted_params[:message][:echo_id],
message_type: :incoming
}
@@ -1,6 +1,5 @@
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
include Events::Types
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
def index
@conversation = conversation
@@ -28,7 +27,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
conversation.contact_last_seen_at = DateTime.now.utc
conversation.save!
::Conversations::UpdateMessageStatusJob.perform_later(conversation.id, conversation.contact_last_seen_at)
head :ok
end
@@ -43,6 +41,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def toggle_typing
head :ok && return if conversation.nil?
case permitted_params[:typing_status]
when 'on'
trigger_typing_event(CONVERSATION_TYPING_ON)
@@ -54,6 +54,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def toggle_status
return head :not_found if conversation.nil?
return head :forbidden unless @web_widget.end_conversation?
unless conversation.resolved?
@@ -79,10 +81,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
end
def render_not_found_if_empty
return head :not_found if conversation.nil?
end
def permitted_params
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
message: [:content, :referer_url, :timestamp, :echo_id],
@@ -64,7 +64,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
def permitted_params
# timestamp parameter is used in create conversation method
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id, :reply_to])
params.permit(:id, :before, :after, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
end
def set_message
@@ -23,7 +23,6 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
def update_last_seen
@conversation.contact_last_seen_at = DateTime.now.utc
@conversation.save!
::Conversations::UpdateMessageStatusJob.perform_later(@conversation.id, @conversation.contact_last_seen_at)
head :ok
end
@@ -39,6 +39,10 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B
)
end
def portal
@portal ||= Portal.find_by!(slug: permitted_params[:slug], archived: false)
end
def list_params
params.permit(:query, :locale, :sort, :status)
end
@@ -14,14 +14,10 @@ class Public::Api::V1::Portals::BaseController < PublicController
@theme = if %w[dark light].include?(params[:theme])
params[:theme]
else
'system'
''
end
end
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
end
def set_locale(&)
switch_locale_with_portal(&) if params[:locale].present?
switch_locale_with_article(&) if params[:article_slug].present?
@@ -44,8 +40,6 @@ class Public::Api::V1::Portals::BaseController < PublicController
def switch_locale_with_article(&)
article = Article.find_by(slug: params[:article_slug])
Rails.logger.info "Article: not found for slug: #{params[:article_slug]}"
render_404 && return if article.blank?
@locale = if article.category.present?
article.category.locale
@@ -59,9 +53,4 @@ class Public::Api::V1::Portals::BaseController < PublicController
def allow_iframe_requests
response.headers.delete('X-Frame-Options') if @is_plain_layout_enabled
end
def render_404
portal
render 'public/api/v1/portals/error/404', status: :not_found
end
end
@@ -13,9 +13,10 @@ class Public::Api::V1::Portals::CategoriesController < Public::Api::V1::Portals:
private
def set_category
@category = @portal.categories.find_by(locale: params[:locale], slug: params[:category_slug])
@category = @portal.categories.find_by!(locale: params[:locale], slug: params[:category_slug])
end
Rails.logger.info "Category: not found for slug: #{params[:category_slug]}"
render_404 && return if @category.blank?
def portal
@portal ||= Portal.find_by!(slug: params[:slug], archived: false)
end
end
@@ -1,21 +0,0 @@
class Twilio::DeliveryStatusController < ApplicationController
def create
::Twilio::DeliveryStatusService.new(params: permitted_params).perform
head :no_content
end
private
def permitted_params
params.permit(
:AccountSid,
:From,
:MessageSid,
:MessagingServiceSid,
:MessageStatus,
:ErrorCode,
:ErrorMessage
)
end
end
+2 -39
View File
@@ -1,48 +1,11 @@
module PortalHelper
def generate_portal_bg_color(portal_color, theme)
base_color = theme == 'dark' ? 'black' : 'white'
"color-mix(in srgb, #{portal_color} 20%, #{base_color})"
"color-mix(in srgb, #{portal_color} 10%, #{base_color})"
end
def generate_portal_bg(portal_color, theme)
bg_image = theme == 'dark' ? 'hexagon-dark.svg' : 'hexagon-light.svg'
bg_image = theme == 'dark' ? 'grid_dark.svg' : 'grid.svg'
"background: url(/assets/images/hc/#{bg_image}) #{generate_portal_bg_color(portal_color, theme)}"
end
def language_name(locale)
language_map = YAML.load_file(Rails.root.join('config/languages/language_map.yml'))
language_map[locale] || locale
end
def get_theme_names(theme)
if theme == 'light'
I18n.t('public_portal.header.appearance.light')
elsif theme == 'dark'
I18n.t('public_portal.header.appearance.dark')
else
I18n.t('public_portal.header.appearance.system')
end
end
def get_theme_icon(theme)
if theme == 'light'
'icons/sun'
elsif theme == 'dark'
'icons/moon'
else
'icons/monitor'
end
end
def generate_gradient_to_bottom(theme)
"background-image: linear-gradient(to bottom, transparent, #{theme == 'dark' ? '#151718' : 'white'})"
end
def generate_article_link(portal_slug, article_slug, theme)
"/hc/#{portal_slug}/articles/#{article_slug}#{theme.present? && theme != 'system' ? "?theme=#{theme}" : ''}"
end
def render_category_content(content)
ChatwootMarkdownRenderer.new(content).render_markdown_to_plain_text
end
end
-5
View File
@@ -84,11 +84,6 @@ export default {
return axios.delete(endPoints('deleteAvatar').url);
},
resetPassword({ email }) {
const urlData = endPoints('resetPassword');
return axios.post(urlData.url, { email });
},
setActiveAccount({ accountId }) {
const urlData = endPoints('setActiveAccount');
return axios.put(urlData.url, {
@@ -1,7 +1,6 @@
/* global axios */
import PortalsAPI from './portals';
import { getArticleSearchURL } from 'dashboard/helper/URLHelper.js';
class ArticlesAPI extends PortalsAPI {
constructor() {
@@ -13,31 +12,14 @@ class ArticlesAPI extends PortalsAPI {
portalSlug,
locale,
status,
authorId,
categorySlug,
sort,
author_id,
category_slug,
}) {
const url = getArticleSearchURL({
pageNumber,
portalSlug,
locale,
status,
authorId,
categorySlug,
sort,
host: this.url,
});
return axios.get(url);
}
searchArticles({ portalSlug, query }) {
const url = getArticleSearchURL({
portalSlug,
query,
host: this.url,
});
return axios.get(url);
let baseUrl = `${this.url}/${portalSlug}/articles?page=${pageNumber}&locale=${locale}`;
if (status !== undefined) baseUrl += `&status=${status}`;
if (author_id) baseUrl += `&author_id=${author_id}`;
if (category_slug) baseUrl += `&category_slug=${category_slug}`;
return axios.get(baseUrl);
}
getArticle({ id, portalSlug }) {
@@ -17,10 +17,6 @@ class PortalsAPI extends ApiClient {
deletePortal(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}`);
}
deleteLogo(portalSlug) {
return axios.delete(`${this.url}/${portalSlug}/logo`);
}
}
export default PortalsAPI;
@@ -26,13 +26,9 @@ export const buildCreatePayload = ({
payload.append('echo_id', echoId);
payload.append('cc_emails', ccEmails);
payload.append('bcc_emails', bccEmails);
if (toEmails) {
payload.append('to_emails', toEmails);
}
if (contentAttributes) {
payload.append('content_attributes', JSON.stringify(contentAttributes));
}
} else {
payload = {
content: message,
@@ -34,7 +34,7 @@ describe('#PortalAPI', () => {
portalSlug: 'room-rental',
locale: 'en-US',
status: 'published',
authorId: '1',
author_id: '1',
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?page=1&locale=en-US&status=published&author_id=1'
@@ -85,33 +85,6 @@ describe('#PortalAPI', () => {
window.axios = originalAxios;
});
it('#searchArticles', () => {
articlesAPI.searchArticles({
query: 'test',
portalSlug: 'room-rental',
});
expect(axiosMock.get).toHaveBeenCalledWith(
'/api/v1/portals/room-rental/articles?query=test'
);
});
});
describe('API calls', () => {
const originalAxios = window.axios;
const axiosMock = {
post: jest.fn(() => Promise.resolve()),
get: jest.fn(() => Promise.resolve()),
patch: jest.fn(() => Promise.resolve()),
delete: jest.fn(() => Promise.resolve()),
};
beforeEach(() => {
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#updateArticle', () => {
articlesAPI.updateArticle({
articleId: 1,
@@ -50,7 +50,6 @@ describe('#ConversationAPI', () => {
message: 'test content',
echoId: 12,
isPrivate: true,
contentAttributes: { in_reply_to: 12 },
files: [new Blob(['test-content'], { type: 'application/pdf' })],
});
expect(formPayload).toBeInstanceOf(FormData);
@@ -58,10 +57,6 @@ describe('#ConversationAPI', () => {
expect(formPayload.get('echo_id')).toEqual('12');
expect(formPayload.get('private')).toEqual('true');
expect(formPayload.get('cc_emails')).toEqual('');
expect(formPayload.get('bcc_emails')).toEqual('');
expect(formPayload.get('content_attributes')).toEqual(
'{"in_reply_to":12}'
);
});
it('builds object payload if file is not available', () => {
@@ -1,5 +1,9 @@
@import '~vue2-datepicker/scss/index';
.mx-datepicker-popup {
@apply z-[99999];
}
.date-picker {
&.no-margin {
.mx-input {
@@ -25,52 +29,10 @@
.mx-input[readonly] {
@apply bg-white dark:bg-slate-900 cursor-pointer;
}
.mx-icon-calendar {
@apply dark:text-slate-500;
}
}
.mx-datepicker-main {
@apply border-0 bg-white dark:bg-slate-800;
.cell {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
&:hover,
&.hover-in-range,
&.in-range {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
}
}
.mx-time {
@apply border-0 bg-white dark:bg-slate-800;
.mx-time-header {
@apply border-0;
}
.mx-time-item {
&.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
&:hover {
@apply bg-slate-75 dark:bg-slate-700;
}
}
}
.today {
@apply font-semibold;
}
}
.mx-datepicker-popup {
@apply z-[99999];
.mx-calendar-content .cell:hover {
@apply bg-slate-75 dark:bg-slate-700 text-slate-900 dark:text-slate-100;
}
.mx-datepicker-inline {
@@ -79,4 +41,24 @@
.mx-calendar {
@apply w-full;
}
.cell.disabled {
@apply bg-slate-25 dark:bg-slate-900 text-slate-200 dark:text-slate-300;
}
.mx-time-item.disabled {
@apply bg-slate-25 dark:bg-slate-900;
}
.today {
@apply font-semibold;
}
.mx-datepicker-main {
@apply border-0 bg-white dark:bg-slate-800;
}
.mx-time-header {
@apply border-0;
}
}
@@ -16,23 +16,3 @@
width: 100%;
}
}
.integration-hooks {
.formulate-input[data-type='checkbox'] {
.formulate-input-wrapper {
@apply flex;
.formulate-input-element {
@apply pr-2;
input {
@apply mb-0;
}
}
}
.formulate-input-element-decorator {
@apply hidden;
}
}
}
@@ -207,7 +207,6 @@ import DeleteCustomViews from 'dashboard/routes/dashboard/customviews/DeleteCust
import ConversationBulkActions from './widgets/conversation/conversationBulkActions/Index.vue';
import alertMixin from 'shared/mixins/alertMixin';
import filterMixin from 'shared/mixins/filterMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import languages from 'dashboard/components/widgets/conversation/advancedFilterItems/languages';
import countries from 'shared/constants/countries';
import { generateValuesForEditCustomViews } from 'dashboard/helper/customViewsHelper';
@@ -239,7 +238,6 @@ export default {
eventListenerMixins,
alertMixin,
filterMixin,
uiSettingsMixin,
],
props: {
conversationInbox: {
@@ -410,12 +408,8 @@ export default {
},
conversationListPagination() {
const conversationsPerPage = 25;
const hasChatsOnView =
this.chatsOnView &&
Array.isArray(this.chatsOnView) &&
!this.chatsOnView.length;
const isNoFiltersOrFoldersAndChatListNotEmpty =
!this.hasAppliedFiltersOrActiveFolders && hasChatsOnView;
!this.hasAppliedFiltersOrActiveFolders && this.chatsOnView !== [];
const isUnderPerPage =
this.chatsOnView.length < conversationsPerPage &&
this.activeAssigneeTabCount < conversationsPerPage &&
@@ -521,7 +515,6 @@ export default {
},
},
mounted() {
this.setFiltersFromUISettings();
this.$store.dispatch('setChatStatusFilter', this.activeStatus);
this.$store.dispatch('setChatSortFilter', this.activeSortBy);
this.resetAndFetchData();
@@ -551,12 +544,6 @@ export default {
this.$store.dispatch('customViews/update', payloadData);
this.closeAdvanceFiltersModal();
},
setFiltersFromUISettings() {
const { conversations_filter_by: filterBy = {} } = this.uiSettings;
const { status, order_by: orderBy } = filterBy;
this.activeStatus = status || wootConstants.STATUS_TYPE.OPEN;
this.activeSortBy = orderBy || wootConstants.SORT_BY_TYPE.LATEST;
},
onClickOpenAddFoldersModal() {
this.showAddFoldersModal = true;
},
@@ -136,7 +136,6 @@ export default {
toStateName: 'labels_list',
showModalForNewItem: true,
modalName: 'AddLabel',
dataTestid: 'sidebar-new-label-button',
children: this.labels.map(label => ({
id: label.id,
label: label.title,
@@ -78,7 +78,6 @@
variant="clear"
color-scheme="secondary"
icon="add"
:data-testid="menuItem.dataTestid"
@click="e => newLinkClick(e, navigate)"
>
{{ $t(`SIDEBAR.${menuItem.newLinkTag}`) }}
@@ -61,7 +61,6 @@ export default {
computed: {
...mapGetters({
currentChat: 'getSelectedChat',
isAChatwootInstance: 'globalConfig/isAChatwootInstance',
}),
isAICTAModalDismissed() {
return this.uiSettings.is_open_ai_cta_modal_dismissed;
@@ -71,8 +70,7 @@ export default {
return (
this.isAdmin &&
!this.isAIIntegrationEnabled &&
!this.isAICTAModalDismissed &&
this.isAChatwootInstance
!this.isAICTAModalDismissed
);
},
// Display a AI CTA button for agents and other admins who have not yet opened the AI assistance modal.
@@ -135,7 +135,6 @@ export default {
// allowSignature is a kill switch, ensuring no signature methods
// are triggered except when this flag is true
allowSignature: { type: Boolean, default: false },
channelType: { type: String, default: '' },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
},
data() {
@@ -267,11 +266,8 @@ export default {
sendWithSignature() {
// this is considered the source of truth, we watch this property
// on change, we toggle the signature in the editor
if (this.allowSignature && !this.isPrivate && this.channelType) {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
}
return false;
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled && this.allowSignature && !this.isPrivate;
},
},
watch: {
@@ -446,7 +442,7 @@ export default {
this.onFocus();
},
click: () => {
this.isEditorMouseFocusedOnAnImage();
// this.isEditorMouseFocusedOnAnImage(); Enable it when the backend supports for message resize is done.
},
blur: () => {
this.onBlur();
@@ -674,7 +670,7 @@ export default {
() => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME
);
this.updateImgToolbarOnDelete();
// this.updateImgToolbarOnDelete(); Enable it when the backend supports for message resize is done.
},
onKeydown(event) {
if (this.isEnterToSendEnabled()) {
@@ -101,7 +101,7 @@
<transition name="modal-fade">
<div
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-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent"
class="flex items-center justify-center gap-2 fixed left-0 right-0 top-0 bottom-0 w-full h-full z-20 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col"
>
<fluent-icon icon="cloud-backup" size="40" />
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
@@ -109,16 +109,6 @@
</h4>
</div>
</transition>
<woot-button
v-if="enableInsertArticleInReply"
v-tooltip.top-end="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
icon="document-text-link"
color-scheme="secondary"
variant="smooth"
size="small"
:title="$t('HELP_CENTER.ARTICLE_SEARCH.OPEN_ARTICLE_SEARCH')"
@click="toggleInsertArticle"
/>
</div>
<div class="right-wrap">
<woot-button
@@ -243,10 +233,6 @@ export default {
type: Boolean,
default: false,
},
portalSlug: {
type: String,
required: true,
},
},
computed: {
...mapGetters({
@@ -313,21 +299,14 @@ export default {
return !this.isOnPrivateNote;
},
sendWithSignature() {
// channelType is sourced from inboxMixin
return this.fetchSignatureFlagFromUiSettings(this.channelType);
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled;
},
signatureToggleTooltip() {
return this.sendWithSignature
? this.$t('CONVERSATION.FOOTER.DISABLE_SIGN_TOOLTIP')
: this.$t('CONVERSATION.FOOTER.ENABLE_SIGN_TOOLTIP');
},
enableInsertArticleInReply() {
const isFeatEnabled = this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.INSERT_ARTICLE_IN_REPLY
);
return isFeatEnabled && this.portalSlug;
},
},
mounted() {
ActiveStorage.start();
@@ -339,14 +318,13 @@ export default {
}
},
toggleMessageSignature() {
this.setSignatureFlagForInbox(this.channelType, !this.sendWithSignature);
this.updateUISettings({
send_with_signature: !this.sendWithSignature,
});
},
replaceText(text) {
this.$emit('replace-text', text);
},
toggleInsertArticle() {
this.$emit('toggle-insert-article');
},
},
};
</script>
@@ -32,7 +32,7 @@
}}</span>
<filter-item
type="sort"
:selected-value="sortFilter"
:selected-value="chatSortFilter"
:items="chatSortItems"
path-prefix="CHAT_LIST.CHAT_SORT_FILTER_ITEMS"
@onChangeFilter="onChangeFilter"
@@ -47,13 +47,12 @@ import wootConstants from 'dashboard/constants/globals';
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import FilterItem from './FilterItem.vue';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
export default {
components: {
FilterItem,
},
mixins: [clickaway, uiSettingsMixin],
mixins: [clickaway],
data() {
return {
showActionsDropdown: false,
@@ -84,17 +83,8 @@ export default {
closeDropdown() {
this.showActionsDropdown = false;
},
onChangeFilter(value, type) {
this.$emit('changeFilter', value, type);
this.saveSelectedFilter(type, value);
},
saveSelectedFilter(type, value) {
this.updateUISettings({
conversations_filter_by: {
status: type === 'status' ? value : this.chatStatus,
order_by: type === 'sort' ? value : this.sortFilter,
},
});
onChangeFilter(type, value) {
this.$emit('changeFilter', type, value);
},
},
};
@@ -58,7 +58,7 @@
<message-preview
v-if="lastMessageInChat"
:message="lastMessageInChat"
class="conversation--message my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] text-sm text-slate-700 dark:text-slate-200"
class="conversation--message mt-0 mb-2 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] text-sm text-slate-700 dark:text-slate-200"
/>
<p
v-else
@@ -3,10 +3,10 @@
class="bg-white dark:bg-slate-900 flex justify-between items-center py-2 px-4 border-b border-slate-50 dark:border-slate-800/50 flex-col md:flex-row"
>
<div
class="flex-1 w-full min-w-0 flex flex-col md:flex-row items-center justify-center"
class="flex-1 w-100 flex flex-col md:flex-row items-center justify-center"
>
<div
class="flex justify-start items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0 w-[inherit]"
class="flex justify-center items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0"
>
<back-button
v-if="showBackButton"
@@ -19,31 +19,26 @@
:username="currentContact.name"
:status="currentContact.availability_status"
/>
<div
class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0 w-[inherit] overflow-hidden"
>
<div class="flex items-center flex-row gap-1 m-0 p-0 w-[inherit]">
<woot-button
variant="link"
color-scheme="secondary"
class="[&>span]:overflow-hidden [&>span]:whitespace-nowrap [&>span]:text-ellipsis min-w-0"
@click.prevent="$emit('contact-panel-toggle')"
<div class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0">
<woot-button
variant="link"
color-scheme="secondary"
class="overflow-hidden whitespace-nowrap text-ellipsis"
@click.prevent="$emit('contact-panel-toggle')"
>
<h3
class="text-base inline-block leading-tight capitalize m-0 p-0 text-ellipsis overflow-hidden whitespace-nowrap text-slate-900 dark:text-slate-100"
>
<span
class="text-base leading-tight text-slate-900 dark:text-slate-100"
>
{{ currentContact.name }}
</span>
</woot-button>
<fluent-icon
v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
size="14"
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0 min-w-[14px]"
icon="warning"
/>
</div>
<span>{{ currentContact.name }}</span>
<fluent-icon
v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
size="14"
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0.5"
icon="warning"
/>
</h3>
</woot-button>
<div
class="conversation--header--actions items-center flex text-xs gap-2 text-ellipsis overflow-hidden whitespace-nowrap"
>
@@ -22,12 +22,22 @@
:bcc="emailHeadAttributes.bcc"
:is-incoming="isIncoming"
/>
<instagram-story-reply v-if="storyUrl" :story-url="storyUrl" />
<blockquote v-if="storyReply" class="story-reply-quote">
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<bubble-image
v-if="!hasImgStoryError && storyUrl"
:url="storyUrl"
@error="onStoryLoadError"
/>
<bubble-video
v-else-if="hasImgStoryError && storyUrl"
:url="storyUrl"
/>
</blockquote>
<bubble-reply-to
v-if="inReplyToMessageId && inboxSupportsReplyTo.incoming"
v-if="inReplyToMessageId && inboxSupportsReplyTo"
:message="inReplyTo"
:message-type="data.message_type"
:parent-has-attachments="hasAttachments"
@click="navigateToMessage"
/>
<bubble-text
v-if="data.content"
@@ -48,15 +58,10 @@
</span>
<div v-if="!isPending && hasAttachments">
<div v-for="attachment in attachments" :key="attachment.id">
<instagram-story
v-if="isAnInstagramStory"
:story-url="attachment.data_url"
@error="onMediaLoadError"
/>
<bubble-image-audio-video
v-else-if="isAttachmentImageVideoAudio(attachment.file_type)"
v-if="isAttachmentImageVideoAudio(attachment.file_type)"
:attachment="attachment"
@error="onMediaLoadError"
@error="onImageLoadError"
/>
<bubble-location
v-else-if="attachment.file_type === 'location'"
@@ -69,6 +74,9 @@
:name="data.content"
:phone-number="attachment.fallback_title"
/>
<instagram-image-error-placeholder
v-else-if="hasImageError && hasInstagramStory"
/>
<bubble-file v-else :url="attachment.data_url" />
</div>
</div>
@@ -80,6 +88,7 @@
:story-id="`${storyId}`"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-email="isEmailContentType"
:is-private="data.private"
:message-type="data.message_type"
@@ -128,18 +137,19 @@
<script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import BubbleActions from './bubble/Actions.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleFile from './bubble/File.vue';
import BubbleImage from './bubble/Image.vue';
import BubbleVideo from './bubble/Video.vue';
import BubbleImageAudioVideo from './bubble/ImageAudioVideo.vue';
import BubbleIntegration from './bubble/Integration.vue';
import BubbleLocation from './bubble/Location.vue';
import BubbleMailHead from './bubble/MailHead.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue';
import BubbleText from './bubble/Text.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import InstagramStory from './bubble/InstagramStory.vue';
import InstagramStoryReply from './bubble/InstagramStoryReply.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue';
import Spinner from 'shared/components/Spinner.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
import alertMixin from 'shared/mixins/alertMixin';
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
@@ -152,18 +162,19 @@ import { LocalStorage } from 'shared/helpers/localStorage';
export default {
components: {
BubbleActions,
BubbleContact,
BubbleFile,
BubbleImage,
BubbleVideo,
BubbleImageAudioVideo,
BubbleIntegration,
BubbleLocation,
BubbleMailHead,
BubbleReplyTo,
BubbleText,
BubbleContact,
BubbleReplyTo,
ContextMenu,
InstagramStory,
InstagramStoryReply,
Spinner,
instagramImageErrorPlaceholder,
},
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin],
props: {
@@ -171,6 +182,10 @@ export default {
type: Object,
required: true,
},
currentChat: {
type: Object,
required: true,
},
isATweet: {
type: Boolean,
default: false,
@@ -179,25 +194,26 @@ export default {
type: Boolean,
default: false,
},
hasInstagramStory: {
type: Boolean,
default: false,
},
isWebWidgetInbox: {
type: Boolean,
default: false,
},
inboxSupportsReplyTo: {
type: Object,
default: () => ({}),
},
inReplyTo: {
type: Object,
default: () => ({}),
type: Boolean,
default: false,
},
},
data() {
return {
showContextMenu: false,
hasMediaLoadError: false,
hasImageError: false,
contextMenuPosition: {},
showBackgroundHighlight: false,
hasImgStoryError: false,
};
},
computed: {
@@ -267,28 +283,33 @@ export default {
);
},
inReplyToMessageId() {
// Why not use the inReplyTo object directly?
// Glad you asked! The inReplyTo object may or may not be available
// depending on the current scroll position of the message list
// since old messages are only loaded when the user scrolls up
return this.data.content_attributes?.in_reply_to;
},
isAnInstagramStory() {
return this.contentAttributes.image_type === 'story_mention';
inReplyTo() {
if (!this.inReplyToMessageId) return null;
const replyTo = this.currentChat?.messages.find(message => {
if (message.id === this.inReplyToMessageId) {
return true;
}
return false;
});
return replyTo ?? {};
},
contextMenuEnabledOptions() {
return {
copy: this.hasText,
delete: this.hasText || this.hasAttachments,
cannedResponse: this.isOutgoing && this.hasText,
replyTo: !this.data.private && this.inboxSupportsReplyTo.outgoing,
replyTo: !this.data.private && this.inboxSupportsReplyTo,
};
},
contentAttributes() {
return this.data.content_attributes || {};
},
externalError() {
return this.contentAttributes.external_error || '';
return this.contentAttributes.external_error || null;
},
sender() {
return this.data.sender || {};
@@ -305,6 +326,9 @@ export default {
storyUrl() {
return this.contentAttributes.story_url || null;
},
storyReply() {
return this.storyUrl && this.hasInstagramStory;
},
contentType() {
const {
data: { content_type: contentType },
@@ -385,7 +409,7 @@ export default {
return false;
}
if (this.isFailed) {
return this.externalError || this.$t(`CONVERSATION.SEND_FAILED`);
return this.externalError ? '' : this.$t(`CONVERSATION.SEND_FAILED`);
}
return false;
},
@@ -443,11 +467,13 @@ export default {
},
watch: {
data() {
this.hasMediaLoadError = false;
this.hasImageError = false;
this.hasImgStoryError = false;
},
},
mounted() {
this.hasMediaLoadError = false;
this.hasImageError = false;
this.hasImgStoryError = false;
bus.$on(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL, this.closeContextMenu);
this.setupHighlightTimer();
},
@@ -457,13 +483,13 @@ export default {
},
methods: {
isAttachmentImageVideoAudio(fileType) {
return ['image', 'audio', 'video', 'story_mention'].includes(fileType);
return ['image', 'audio', 'video'].includes(fileType);
},
hasMediaAttachment(type) {
if (this.hasAttachments && this.data.attachments.length > 0) {
const { attachments = [{}] } = this.data;
const { file_type: fileType } = attachments[0];
return fileType === type && !this.hasMediaLoadError;
return fileType === type && !this.hasImageError;
}
if (this.storyReply) {
return true;
@@ -476,8 +502,11 @@ export default {
async retrySendMessage() {
await this.$store.dispatch('sendMessageWithData', this.data);
},
onMediaLoadError() {
this.hasMediaLoadError = true;
onImageLoadError() {
this.hasImageError = true;
},
onStoryLoadError() {
this.hasImgStoryError = true;
},
openContextMenu(e) {
const shouldSkipContextMenu =
@@ -519,6 +548,13 @@ export default {
this.showBackgroundHighlight = false;
}, HIGHLIGHT_TIMER);
},
navigateToMessage() {
this.$nextTick(() => {
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE, {
messageId: this.inReplyToMessageId,
});
});
},
},
};
</script>
@@ -709,4 +745,8 @@ li.right {
}
}
}
.story-reply-quote {
@apply mt-2 mx-4 mb-0 px-2 pb-0 pt-2 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200;
}
</style>
@@ -1,5 +1,5 @@
<template>
<div class="overflow-hidden text-ellipsis whitespace-nowrap">
<span class="overflow-hidden text-ellipsis whitespace-nowrap">
<template v-if="showMessageType">
<fluent-icon
v-if="isMessagePrivate"
@@ -35,7 +35,7 @@
<span v-else>
{{ defaultEmptyMessage || $t('CHAT_LIST.NO_CONTENT') }}
</span>
</div>
</span>
</template>
<script>
@@ -12,7 +12,7 @@
variant="smooth"
size="tiny"
color-scheme="secondary"
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[9.5rem] md:top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
:icon="isRightOrLeftIcon"
@click="onToggleContactPanel"
/>
@@ -31,9 +31,10 @@
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
:current-chat="currentChat"
/>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<span>
@@ -53,9 +54,10 @@
:data="message"
:is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
:current-chat="currentChat"
/>
<conversation-label-suggestion
v-if="shouldShowLabelSuggestions"
@@ -114,6 +116,7 @@ import { LocalStorage } from 'shared/helpers/localStorage';
// constants
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@@ -235,6 +238,10 @@ export default {
isATweet() {
return this.conversationType === 'tweet';
},
hasInstagramStory() {
return this.conversationType === 'instagram_direct_message';
},
isRightOrLeftIcon() {
if (this.isContactPanelOpen) {
return 'arrow-chevron-right';
@@ -284,10 +291,13 @@ export default {
return this.currentChat.unread_count || 0;
},
inboxSupportsReplyTo() {
return {
incoming: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO),
outgoing: this.inboxHasFeature(INBOX_FEATURES.REPLY_TO_OUTGOING),
};
return (
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
);
},
},
@@ -340,7 +350,7 @@ export default {
// method available in mixin, need to ensure that integrations are present
await this.fetchIntegrationsIfRequired();
if (!this.isLabelSuggestionFeatureEnabled) {
if (!this.isAIIntegrationEnabled) {
return;
}
@@ -498,19 +508,6 @@ export default {
makeMessagesRead() {
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
},
getInReplyToMessage(parentMessage) {
if (!parentMessage) return {};
const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to;
if (!inReplyToMessageId) return {};
return this.currentChat?.messages.find(message => {
if (message.id === inReplyToMessageId) {
return true;
}
return false;
});
},
},
};
</script>
@@ -18,17 +18,12 @@
:popout-reply-box="popoutReplyBox"
@click="$emit('click')"
/>
<article-search-popover
v-if="showArticleSearchPopover && connectedPortalSlug"
:selected-portal-slug="connectedPortalSlug"
@insert="handleInsert"
@close="onSearchPopoverClose"
/>
<div class="reply-box__top">
<reply-to-message
v-if="shouldShowReplyToMessage"
:message="inReplyTo"
@dismiss="resetReplyToMessage"
@navigate-to-message="navigateToMessage"
/>
<canned-response
v-if="showMentions && hasSlashCommand"
@@ -41,7 +36,7 @@
v-if="showEmojiPicker"
v-on-clickaway="hideEmojiPicker"
:class="emojiDialogClassOnExpandedLayoutAndRTLView"
:on-click="addIntoEditor"
:on-click="emojiOnClick"
/>
<reply-email-head
v-if="showReplyHead"
@@ -85,7 +80,6 @@
:variables="messageVariables"
:signature="signatureToApply"
:allow-signature="true"
:channel-type="channelType"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@@ -128,12 +122,10 @@
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
:message="message"
:portal-slug="connectedPortalSlug"
:new-conversation-modal-active="newConversationModalActive"
@selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
@replace-text="replaceText"
@toggle-insert-article="toggleInsertArticle"
/>
<whatsapp-templates
:inbox-id="inbox.id"
@@ -163,7 +155,6 @@ import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.v
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
import ReplyEmailHead from './ReplyEmailHead.vue';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue';
import ArticleSearchPopover from 'dashboard/routes/dashboard/helpcenter/components/ArticleSearch/SearchPopover.vue';
import MessageSignatureMissingAlert from './MessageSignatureMissingAlert';
import Banner from 'dashboard/components/ui/Banner.vue';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
@@ -194,6 +185,7 @@ import {
replaceSignature,
extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
@@ -215,7 +207,6 @@ export default {
Banner,
WhatsappTemplates,
MessageSignatureMissingAlert,
ArticleSearchPopover,
},
mixins: [
clickaway,
@@ -258,7 +249,6 @@ export default {
showCannedMenu: false,
showVariablesMenu: false,
newConversationModalActive: false,
showArticleSearchPopover: false,
};
},
computed: {
@@ -275,7 +265,11 @@ export default {
return (
this.inReplyTo?.id &&
!this.isPrivate &&
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO)
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
);
},
showRichContentEditor() {
@@ -473,7 +467,8 @@ export default {
return !!this.signatureToApply;
},
sendWithSignature() {
return this.fetchSignatureFlagFromUiSettings(this.channelType);
const { send_with_signature: isEnabled } = this.uiSettings;
return isEnabled;
},
editorMessageKey() {
const { editor_message_key: isEnabled } = this.uiSettings;
@@ -512,18 +507,11 @@ export default {
? this.messageSignature
: extractTextFromMarkdown(this.messageSignature);
},
connectedPortalSlug() {
const { help_center: portal = {} } = this.inbox;
const { slug = '' } = portal;
return slug;
},
},
watch: {
currentChat(conversation) {
const { can_reply: canReply } = conversation;
this.setCCAndToEmailsFromLastChat();
if (this.isOnPrivateNote) {
return;
}
@@ -534,7 +522,7 @@ export default {
this.replyType = REPLY_EDITOR_MODES.NOTE;
}
this.fetchAndSetReplyTo();
this.setCCAndToEmailsFromLastChat();
},
conversationIdByRoute(conversationId, oldConversationId) {
if (conversationId !== oldConversationId) {
@@ -543,25 +531,17 @@ export default {
}
},
message(updatedMessage) {
// Check if the message starts with a slash.
const bodyWithoutSignature = removeSignature(
updatedMessage,
this.signatureToApply
);
const startsWithSlash = bodyWithoutSignature.startsWith('/');
// Determine if the user is potentially typing a slash command.
// This is true if the message starts with a slash and the rich content editor is not active.
this.hasSlashCommand = startsWithSlash && !this.showRichContentEditor;
this.showMentions = this.hasSlashCommand;
// If a slash command is active, extract the command text after the slash.
// If not, reset the mentionSearchKey.
this.mentionSearchKey = this.hasSlashCommand
? bodyWithoutSignature.substring(1)
: '';
// Autosave the current message draft.
this.hasSlashCommand =
updatedMessage[0] === '/' && !this.showRichContentEditor;
const hasNextWord = updatedMessage.includes(' ');
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
if (isShortCodeActive) {
this.mentionSearchKey = updatedMessage.substring(1);
this.showMentions = true;
} else {
this.mentionSearchKey = '';
this.showMentions = false;
}
this.doAutoSaveDraft();
},
replyType(updatedReplyType, oldReplyType) {
@@ -608,23 +588,6 @@ export default {
);
},
methods: {
handleInsert(article) {
const { url, title } = article;
if (this.isRichEditorEnabled) {
// Removing empty lines from the title
const lines = title.split('\n');
const nonEmptyLines = lines.filter(line => line.trim() !== '');
const filteredMarkdown = nonEmptyLines.join(' ');
bus.$emit(
BUS_EVENTS.INSERT_INTO_RICH_EDITOR,
`[${filteredMarkdown}](${url})`
);
} else {
this.addIntoEditor(
`${this.$t('CONVERSATION.REPLYBOX.INSERT_READ_MORE')} ${url}`
);
}
},
toggleRichContentEditor() {
this.updateUISettings({
display_rich_content_editor: !this.showRichContentEditor,
@@ -805,7 +768,6 @@ export default {
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
channelType: this.channelType,
signatureEnabled: this.sendWithSignature,
hasReplyTo: !!this.inReplyTo?.id,
});
},
async onSendReply() {
@@ -856,18 +818,10 @@ export default {
this.hideWhatsappTemplatesModal();
},
replaceText(message) {
if (this.sendWithSignature && !this.private) {
// if signature is enabled, append it to the message
// appendSignature ensures that the signature is not duplicated
// so we don't need to check if the signature is already present
message = appendSignature(message, this.signatureToApply);
}
const updatedMessage = replaceVariablesInMessage({
message,
variables: this.messageVariables,
});
setTimeout(() => {
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.message = updatedMessage;
@@ -890,22 +844,22 @@ export default {
clearEditorSelection() {
this.updateEditorSelectionWith = '';
},
insertIntoTextEditor(text, selectionStart, selectionEnd) {
insertEmoji(emoji, selectionStart, selectionEnd) {
const { message } = this;
const newMessage =
message.slice(0, selectionStart) +
text +
emoji +
message.slice(selectionEnd, message.length);
this.message = newMessage;
},
addIntoEditor(content) {
emojiOnClick(emoji) {
if (this.showRichContentEditor) {
this.updateEditorSelectionWith = content;
this.updateEditorSelectionWith = emoji;
this.onFocus();
}
if (!this.showRichContentEditor) {
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
this.insertIntoTextEditor(content, selectionStart, selectionEnd);
this.insertEmoji(emoji, selectionStart, selectionEnd);
}
},
clearMessage() {
@@ -1009,19 +963,6 @@ export default {
(item, index) => itemIndex !== index
);
},
setReplyToInPayload(payload) {
if (this.inReplyTo?.id) {
return {
...payload,
contentAttributes: {
...payload.contentAttributes,
in_reply_to: this.inReplyTo.id,
},
};
}
return payload;
},
getMessagePayloadForWhatsapp(message) {
const multipleMessagePayload = [];
@@ -1031,41 +972,41 @@ export default {
const attachedFile = this.globalConfig.directUploadsEnabled
? attachment.blobSignedId
: attachment.resource.file;
let attachmentPayload = {
const attachmentPayload = {
conversationId: this.currentChat.id,
files: [attachedFile],
private: false,
message: caption,
sender: this.sender,
};
attachmentPayload = this.setReplyToInPayload(attachmentPayload);
multipleMessagePayload.push(attachmentPayload);
caption = '';
});
} else {
let messagePayload = {
const messagePayload = {
conversationId: this.currentChat.id,
message,
private: false,
sender: this.sender,
};
messagePayload = this.setReplyToInPayload(messagePayload);
multipleMessagePayload.push(messagePayload);
}
return multipleMessagePayload;
},
getMessagePayload(message) {
let messagePayload = {
const messagePayload = {
conversationId: this.currentChat.id,
message,
private: this.isPrivate,
sender: this.sender,
};
messagePayload = this.setReplyToInPayload(messagePayload);
if (this.inReplyTo?.id) {
messagePayload.contentAttributes = {
in_reply_to: this.inReplyTo.id,
};
}
if (this.attachedFiles && this.attachedFiles.length) {
messagePayload.files = [];
@@ -1144,7 +1085,7 @@ export default {
this.conversationId
);
this.inReplyTo = this.currentChat?.messages?.find(message => {
this.inReplyTo = this.currentChat.messages.find(message => {
if (message.id === replyToMessageId) {
return true;
}
@@ -1156,6 +1097,11 @@ export default {
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
},
navigateToMessage(messageId) {
this.$nextTick(() => {
bus.$emit(BUS_EVENTS.SCROLL_TO_MESSAGE, { messageId });
});
},
onNewConversationModalActive(isActive) {
// Issue is if the new conversation modal is open and we drag and drop the file
// then the file is not getting attached to the new conversation modal
@@ -1164,12 +1110,6 @@ export default {
// When new conversation modal is open
this.newConversationModalActive = isActive;
},
onSearchPopoverClose() {
this.showArticleSearchPopover = false;
},
toggleInsertArticle() {
this.showArticleSearchPopover = !this.showArticleSearchPopover;
},
},
};
</script>
@@ -1188,7 +1128,7 @@ export default {
}
.reply-box {
@apply relative border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
@apply border-t border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-900;
&.is-private {
@apply bg-yellow-50 dark:bg-yellow-200;
@@ -1,16 +1,12 @@
<template>
<div
class="reply-editor bg-slate-50 dark:bg-slate-800 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
class="reply-editor bg-slate-50 dark:bg-slate-800 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2 cursor-pointer"
@click="$emit('navigate-to-message', messageId)"
>
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" />
<div class="flex-grow gap-1 mt-px text-xs truncate">
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" icon-size="14" />
<div class="flex items-center flex-grow gap-1 text-xs">
{{ $t('CONVERSATION.REPLYBOX.REPLYING_TO') }}
<message-preview
:message="message"
:show-message-type="false"
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
class="inline"
/>
<message-preview :message="message" :show-message-type="false" />
</div>
<woot-button
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
@@ -114,6 +114,10 @@ export default {
type: Boolean,
default: true,
},
hasInstagramStory: {
type: Boolean,
default: true,
},
messageType: {
type: Number,
default: 1,
@@ -192,46 +196,24 @@ export default {
if (!this.showStatusIndicators) {
return false;
}
// Messages will be marked as sent for the Email channel if they have a source ID.
if (this.isAnEmailChannel) {
return !!this.sourceId;
}
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox ||
this.isASmsInbox ||
this.isATelegramChannel
) {
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isSent;
}
// All messages will be mark as sent for the Line channel, as there is no source ID.
if (this.isALineChannel) {
return true;
}
return false;
},
showDeliveredIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isASmsInbox ||
this.isAFacebookInbox
) {
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isDelivered;
}
// All messages marked as delivered for the web widget inbox and API inbox once they are sent.
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isSent;
}
if (this.isALineChannel) {
return this.isDelivered;
}
return false;
},
@@ -239,16 +221,14 @@ export default {
if (!this.showStatusIndicators) {
return false;
}
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox
) {
return this.sourceId && this.isRead;
}
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isRead;
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt >= this.createdAt;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isRead;
}
return false;
@@ -1,6 +1,6 @@
<template>
<div class="image message-text__wrap">
<img :src="url" @click="onClick" @error="$emit('error')" />
<img :src="url" @click="onClick" @error="onImgError()" />
<woot-modal :full-width="true" :show.sync="show" :on-close="onClose">
<img :src="url" class="modal-image skip-context-menu" />
</woot-modal>
@@ -28,6 +28,9 @@ export default {
onClick() {
this.show = true;
},
onImgError() {
this.$emit('error');
},
},
};
</script>
@@ -4,14 +4,13 @@
v-if="isImage && !isImageError"
:src="attachment.data_url"
@click="onClick"
@error="onImgError"
@error="onImgError()"
/>
<video
v-if="isVideo"
:src="attachment.data_url"
muted
playsInline
@error="onImgError"
@click="onClick"
/>
<audio v-else-if="isAudio" controls class="skip-context-menu">
@@ -22,7 +21,7 @@
:show.sync="show"
:attachment="attachment"
:all-attachments="filteredCurrentChatAttachments"
@error="onImgError"
@error="onImgError()"
@close="onClose"
/>
</div>
@@ -1,51 +0,0 @@
<template>
<bubble-image
v-if="!hasImgStoryError"
:url="storyUrl"
@error="onImageLoadError"
/>
<bubble-video
v-else-if="!hasVideoStoryError"
:url="storyUrl"
@error="onVideoLoadError"
/>
<instagram-story-error-place-holder v-else />
</template>
<script>
import BubbleImage from './Image.vue';
import BubbleVideo from './Video.vue';
import InstagramStoryErrorPlaceHolder from './InstagramStoryErrorPlaceHolder.vue';
export default {
components: {
BubbleImage,
BubbleVideo,
InstagramStoryErrorPlaceHolder,
},
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
this.emitError();
},
onVideoLoadError() {
this.hasVideoStoryError = true;
this.emitError();
},
emitError() {
this.$emit('error');
},
},
};
</script>
@@ -1,14 +0,0 @@
<template>
<div
class="flex items-center justify-center px-8 h-28 w-full bg-slate-100 text-slate-700 dark:bg-slate-500 dark:text-slate-75"
>
<fluent-icon icon="document-error" size="32" />
<p class="mb-0 text-slate-700 dark:text-slate-75">
{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}
</p>
</div>
</template>
<script>
export default {};
</script>
@@ -1,35 +0,0 @@
<template>
<blockquote
class="my-0 px-2 pb-0 pt-0 border-l-4 border-solid border-slate-75 dark:border-slate-600 text-slate-600 dark:text-slate-200"
>
<span>{{ $t('CONVERSATION.REPLIED_TO_STORY') }}</span>
<instagram-story :story-url="storyUrl" class="mt-3 rounded-md" />
</blockquote>
</template>
<script>
import InstagramStory from './InstagramStory.vue';
export default {
components: { InstagramStory },
props: {
storyUrl: {
type: String,
default: '',
},
},
data() {
return {
hasImgStoryError: false,
hasVideoStoryError: false,
};
},
methods: {
onImageLoadError() {
this.hasImgStoryError = true;
},
onVideoLoadError() {
this.hasVideoStoryError = true;
},
},
};
</script>
@@ -1,24 +1,18 @@
<template>
<div
class="px-2 py-1.5 rounded-sm min-w-[10rem] mb-2"
:class="{
'bg-slate-50 dark:bg-slate-600 dark:text-slate-50':
messageType === MESSAGE_TYPE.INCOMING,
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
'-mx-2': !parentHasAttachments,
}"
class="px-2 py-1.5 -mx-2 rounded-md bg-woot-600 text-woot-50 min-w-[15rem] mb-2 cursor-pointer"
@click="$emit('click')"
>
<message-preview
:message="message"
:show-message-type="false"
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
::default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
/>
</div>
</template>
<script>
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
import { MESSAGE_TYPE } from 'shared/constants/messages';
export default {
name: 'ReplyTo',
@@ -30,17 +24,6 @@ export default {
type: Object,
required: true,
},
messageType: {
type: Number,
required: true,
},
parentHasAttachments: {
type: Boolean,
required: true,
},
},
data() {
return { MESSAGE_TYPE };
},
};
</script>
@@ -1,12 +1,12 @@
<template>
<div class="video message-text__wrap">
<video ref="videoElement" :src="url" muted playsInline @click="onClick" />
<video :src="url" muted playsInline @click="onClick" />
<woot-modal :show.sync="show" :on-close="onClose">
<video
:src="url"
controls
playsInline
class="modal-video skip-context-menu mx-auto"
class="modal-video skip-context-menu"
/>
</woot-modal>
</div>
@@ -25,11 +25,6 @@ export default {
show: false,
};
},
mounted() {
this.$refs.videoElement.onerror = () => {
this.$emit('error');
};
},
methods: {
onClose() {
this.show = false;
@@ -0,0 +1,26 @@
<template>
<div class="image-placeholder">
<fluent-icon icon="document-error" size="32" />
<p>{{ $t('COMPONENTS.FILE_BUBBLE.INSTAGRAM_STORY_UNAVAILABLE') }}</p>
</div>
</template>
<script>
export default {};
</script>
<style scoped>
.image-placeholder {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
padding: var(--space-slab);
height: calc(var(--space-large) * 10);
margin-top: var(--space-one);
width: 100%;
background-color: var(--s-75);
color: var(--s-800);
border-radius: var(--border-radius-normal);
}
</style>
+1 -1
View File
@@ -16,5 +16,5 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs',
INSERT_ARTICLE_IN_REPLY: 'insert_article_in_reply',
MESSAGE_REPLY_TO: 'message_reply_to',
};
@@ -3,7 +3,6 @@ export const CONVERSATION_EVENTS = Object.freeze({
SENT_MESSAGE: 'Sent a message',
SENT_PRIVATE_NOTE: 'Sent a private note',
INSERTED_A_CANNED_RESPONSE: 'Inserted a canned response',
TRANSLATE_A_MESSAGE: 'Translated a message',
INSERTED_A_VARIABLE: 'Inserted a variable',
USED_MENTIONS: 'Used mentions',
SEARCH_CONVERSATION: 'Searched conversations',
@@ -64,47 +64,3 @@ export const isValidURL = value => {
/^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;
return URL_REGEX.test(value);
};
export const getArticleSearchURL = ({
host,
portalSlug,
pageNumber,
locale,
status,
authorId,
categorySlug,
sort,
query,
}) => {
const queryParams = new URLSearchParams({});
const params = {
page: pageNumber,
locale,
status,
author_id: authorId,
category_slug: categorySlug,
sort,
query,
};
Object.entries(params).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
queryParams.set(key, value);
}
});
return `${host}/${portalSlug}/articles?${queryParams.toString()}`;
};
export const hasValidAvatarUrl = avatarUrl => {
try {
const { host: avatarUrlHost } = new URL(avatarUrl);
const isFromGravatar = ['www.gravatar.com', 'gravatar'].includes(
avatarUrlHost
);
return avatarUrl && !isFromGravatar;
} catch (error) {
return false;
}
};
@@ -135,7 +135,6 @@ export const getActionOptions = ({ agents, teams, labels, type }) => {
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
remove_label: generateConditionOptions(labels, 'title'),
change_priority: PRIORITY_CONDITION_VALUES,
};
return actionsMap[type];
@@ -3,8 +3,6 @@ import {
conversationUrl,
isValidURL,
conversationListPageURL,
getArticleSearchURL,
hasValidAvatarUrl,
} from '../URLHelper';
describe('#URL Helpers', () => {
@@ -77,117 +75,4 @@ describe('#URL Helpers', () => {
expect(isValidURL('alert.window')).toBe(false);
});
});
describe('getArticleSearchURL', () => {
it('should generate a basic URL without optional parameters', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
it('should include status parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
status: 'published',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&status=published'
);
});
it('should include author_id parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
authorId: 123,
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&author_id=123'
);
});
it('should include category_slug parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
categorySlug: 'technology',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&category_slug=technology'
);
});
it('should include sort parameter if provided', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
sort: 'views',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en&sort=views');
});
it('should handle multiple optional parameters', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
status: 'draft',
authorId: 456,
categorySlug: 'science',
sort: 'views',
host: 'myurl.com',
});
expect(url).toBe(
'myurl.com/news/articles?page=1&locale=en&status=draft&author_id=456&category_slug=science&sort=views'
);
});
it('should handle missing optional parameters gracefully', () => {
const url = getArticleSearchURL({
portalSlug: 'news',
pageNumber: 1,
locale: 'en',
host: 'myurl.com',
});
expect(url).toBe('myurl.com/news/articles?page=1&locale=en');
});
});
describe('hasValidAvatarUrl', () => {
test('should return true for valid non-Gravatar URL', () => {
expect(hasValidAvatarUrl('https://chatwoot.com/avatar.jpg')).toBe(true);
});
test('should return false for a Gravatar URL (www.gravatar.com)', () => {
expect(hasValidAvatarUrl('https://www.gravatar.com/avatar.jpg')).toBe(
false
);
});
test('should return false for a Gravatar URL (gravatar)', () => {
expect(hasValidAvatarUrl('https://gravatar/avatar.jpg')).toBe(false);
});
test('should handle invalid URL', () => {
expect(hasValidAvatarUrl('invalid-url')).toBe(false); // or expect an error, depending on function design
});
test('should return false for empty or undefined URL', () => {
expect(hasValidAvatarUrl('')).toBe(false);
expect(hasValidAvatarUrl()).toBe(false);
});
});
});
@@ -41,16 +41,6 @@
"STATUS": "Status",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Created at:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,10 +204,6 @@
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send message",
"CANCEL": "Cancel",
"SUCCESS_MESSAGE": "Message sent!",
@@ -347,17 +343,17 @@
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contacts attributes will take precedence.",
"PRIMARY": {
"TITLE": "Primary contact",
"HELP_LABEL": "To be deleted"
"HELP_LABEL": "To be kept"
},
"PARENT": {
"CHILD": {
"TITLE": "Contact to merge",
"PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be kept"
"HELP_LABEL": "To be deleted"
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,6 +34,7 @@
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -46,7 +47,6 @@
"NO_RESPONSE": "No response",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,8 +139,7 @@
"PRIVATE_NOTE": "Private Note",
"SEND": "Send",
"CREATE": "Add Note",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TWEET": "Tweet",
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files",
@@ -195,7 +194,6 @@
},
"CONTEXT_MENU": {
"COPY": "Copy",
"REPLY_TO": "Reply to this message",
"DELETE": "Delete",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -1,7 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
"NOT_FOUND": "No emoji match your search"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Name",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -425,33 +425,6 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"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.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"HELP_CENTER": {
"TITLE": "Help Center",
"TITLE": "Help Center (Beta)",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,16 +41,6 @@
"STATUS": "الحالة",
"ORDER_BY": "الترتيب بواسطة"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "تم إنشاؤها في:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "النشاط الأخير:",
"ACTIVE": "النشاط الأخير"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "النشاط الأخير"
@@ -204,10 +204,6 @@
"PLACEHOLDER": "اكتب رسالتك هنا",
"ERROR": "لا يمكن أن تكون الرسالة فارغة"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "إرسال الرسالة",
"CANCEL": "إلغاء",
"SUCCESS_MESSAGE": "تم إرسال الرسالة!",
@@ -347,17 +343,17 @@
"DESCRIPTION": "دمج جهات الاتصال للجمع بين ملفّين شخصيين في واحد، بما في ذلك جميع السمات والمحادثات. في حالة التعارض، تكون لخصائص جهة الاتصال الرئيسية الأسبقية.",
"PRIMARY": {
"TITLE": "جهة الاتصال الرئيسية",
"HELP_LABEL": "ليتم حذفها"
"HELP_LABEL": "سيتم الاحتفاظ بها"
},
"PARENT": {
"CHILD": {
"TITLE": "دمج جهة الإتصال",
"PLACEHOLDER": "بحث عن جهات الاتصال",
"HELP_LABEL": "سيتم الاحتفاظ بها"
"HELP_LABEL": "ليتم حذفها"
},
"SUMMARY": {
"TITLE": "ملخص",
"DELETE_WARNING": "سيتم حذف جهة الاتصال بـ <strong>%{primaryContactName}</strong>.",
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال بـ <strong>%{primaryContactName}</strong> إلى <strong>%{parentContactName}</strong>."
"DELETE_WARNING": "سيتم حذف جهة الاتصال بـ <strong>%{childContactName}</strong>.",
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال بـ <strong>%{childContactName}</strong> إلى <strong>%{primaryContactName}</strong>."
},
"SEARCH": {
"ERROR": "رسالة_خطأ"
@@ -34,6 +34,7 @@
"ASSIGN_TO_ME": "إسناد لي",
"TWILIO_WHATSAPP_CAN_REPLY": "يمكنك فقط الرد على هذه المحادثة باستخدام رسالة قالب بسبب",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
"SELECT_A_TWEET_TO_REPLY": "الرجاء تحديد تغريدة للرد عليها.",
"REPLYING_TO": "أنت ترد على:",
"REMOVE_SELECTION": "إزالة التحديد",
"DOWNLOAD": "تنزيل",
@@ -46,7 +47,6 @@
"NO_RESPONSE": "لا توجد استجابة",
"RATING_TITLE": "التقييم",
"FEEDBACK_TITLE": "الملاحظات",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "إظهار السمات",
"HIDE_LABELS": "إخفاء السمات"
@@ -139,8 +139,7 @@
"PRIVATE_NOTE": "إضافة ملاحظة خاصة",
"SEND": "إرسال",
"CREATE": "إضافة ملاحظة",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TWEET": "تغريد",
"TIP_FORMAT_ICON": "عرض محرر النصوص",
"TIP_EMOJI_ICON": "إظهار قائمة الرموز التعبيرية",
"TIP_ATTACH_ICON": "إرفاق الملفات",
@@ -195,7 +194,6 @@
},
"CONTEXT_MENU": {
"COPY": "نسخ",
"REPLY_TO": "Reply to this message",
"DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "إضافة إلى الردود السريعة",
"TRANSLATE": "ترجم",
@@ -1,7 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "ابحث في الايموجي",
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك",
"REMOVE": "حذف"
"NOT_FOUND": "لا يوجد إيموجي يطابق بحثك"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "الاسم",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "الاسم مطلوب"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "الاسم",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "الاسم مطلوب"
},
"SLUG": {
@@ -425,33 +425,6 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"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.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "اعرف المزيد",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "نظرة عامة",
"FACEBOOK_REAUTHORIZE": "انتهت صلاحية اتصال الفيسبوك الخاص بك، يرجى إعادة الاتصال بصفحة الفيسبوك الخاصة بك لمواصلة الخدمات",
"HELP_CENTER": {
"TITLE": "Help Center",
"TITLE": "مركز المساعدة (نسخة تجريبية)",
"ALL_ARTICLES": "جميع المقالات",
"MY_ARTICLES": "مقالاتي",
"DRAFT": "مسودة",
@@ -41,16 +41,6 @@
"STATUS": "Статус",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Създаден в:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,10 +204,6 @@
"PLACEHOLDER": "Напишете съобщението си тук",
"ERROR": "Съобщението не може да е празно"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Изпрати съобщение",
"CANCEL": "Отмени",
"SUCCESS_MESSAGE": "Съобщението е изпратено!",
@@ -347,17 +343,17 @@
"DESCRIPTION": "Обединете контактите, за да комбинирате два профила в един, включително всички атрибути и разговори. В случай на конфликт атрибутите на основния контакт ще имат предимство.",
"PRIMARY": {
"TITLE": "Основен контакт",
"HELP_LABEL": "Да бъде изтрит"
"HELP_LABEL": "Да бъде запазено"
},
"PARENT": {
"CHILD": {
"TITLE": "Контакт за обединяване",
"PLACEHOLDER": "Търсене на контакт",
"HELP_LABEL": "Да бъде запазено"
"HELP_LABEL": "Да бъде изтрит"
},
"SUMMARY": {
"TITLE": "Резюме",
"DELETE_WARNING": "Контакт от <strong>%{primaryContactName}</strong> ще бъде изтрит.",
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>%{primaryContactName}</strong> ще бъдат копирани в <strong>%{parentContactName}</strong>."
"DELETE_WARNING": "Контакт от <strong>%{childContactName}</strong> ще бъде изтрит.",
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>%{childContactName}</strong> ще бъдат копирани в <strong>%{primaryContactName}</strong>."
},
"SEARCH": {
"ERROR": "ГРЕШКА"
@@ -34,6 +34,7 @@
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -46,7 +47,6 @@
"NO_RESPONSE": "No response",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,8 +139,7 @@
"PRIVATE_NOTE": "Private Note",
"SEND": "Send",
"CREATE": "Add Note",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TWEET": "Tweet",
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files",
@@ -195,7 +194,6 @@
},
"CONTEXT_MENU": {
"COPY": "Copy",
"REPLY_TO": "Reply to this message",
"DELETE": "Изтрий",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -1,7 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Remove"
"NOT_FOUND": "No emoji match your search"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Име",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Име",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -425,33 +425,6 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"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.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"HELP_CENTER": {
"TITLE": "Help Center",
"TITLE": "Help Center (Beta)",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,16 +41,6 @@
"STATUS": "Estat",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Created at:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,10 +204,6 @@
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send message",
"CANCEL": "Cancel·la",
"SUCCESS_MESSAGE": "Message sent!",
@@ -347,17 +343,17 @@
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact s attributes will take precedence.",
"PRIMARY": {
"TITLE": "Primary contact",
"HELP_LABEL": "To be deleted"
"HELP_LABEL": "To be kept"
},
"PARENT": {
"CHILD": {
"TITLE": "Contact to merge",
"PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be kept"
"HELP_LABEL": "To be deleted"
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,6 +34,7 @@
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "Restricció de finestra de missatges de 24 hores",
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
"REPLYING_TO": "Estas responent a:",
"REMOVE_SELECTION": "Elimina la selecció",
"DOWNLOAD": "Descarrega",
@@ -46,7 +47,6 @@
"NO_RESPONSE": "Sense resposta",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,8 +139,7 @@
"PRIVATE_NOTE": "Nota privada",
"SEND": "Envia",
"CREATE": "Afegeix una nota",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TWEET": "Tuit",
"TIP_FORMAT_ICON": "Mostra l'editor de text enriquit",
"TIP_EMOJI_ICON": "Mostra la selecció d'emoticones",
"TIP_ATTACH_ICON": "Ajuntar fitxers",
@@ -195,7 +194,6 @@
},
"CONTEXT_MENU": {
"COPY": "Copia",
"REPLY_TO": "Reply to this message",
"DELETE": "Esborrar",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -1,7 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Suprimeix"
"NOT_FOUND": "No emoji match your search"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Nom",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Nom",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -425,33 +425,6 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"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.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "La teva connexió a Facebook ha caducat, torna a connectar la vostra pàgina de Facebook per continuar els serveis",
"HELP_CENTER": {
"TITLE": "Help Center",
"TITLE": "Help Center (Beta)",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,16 +41,6 @@
"STATUS": "Stav",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Created at:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,10 +204,6 @@
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send message",
"CANCEL": "Zrušit",
"SUCCESS_MESSAGE": "Message sent!",
@@ -347,17 +343,17 @@
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact s attributes will take precedence.",
"PRIMARY": {
"TITLE": "Primary contact",
"HELP_LABEL": "To be deleted"
"HELP_LABEL": "To be kept"
},
"PARENT": {
"CHILD": {
"TITLE": "Contact to merge",
"PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be kept"
"HELP_LABEL": "To be deleted"
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,6 +34,7 @@
"ASSIGN_TO_ME": "Přiřadit mi",
"TWILIO_WHATSAPP_CAN_REPLY": "Na tuto konverzaci můžete odpovědět pouze pomocí šablony zprávy z důvodu",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hodinové omezení okna",
"SELECT_A_TWEET_TO_REPLY": "Vyberte prosím tweet, na který chcete odpovědět.",
"REPLYING_TO": "Odpovídáte uživateli:",
"REMOVE_SELECTION": "Odstranit výběr",
"DOWNLOAD": "Stáhnout",
@@ -46,7 +47,6 @@
"NO_RESPONSE": "Bez odpovědi",
"RATING_TITLE": "Hodnocení",
"FEEDBACK_TITLE": "Zpětná vazba",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,8 +139,7 @@
"PRIVATE_NOTE": "Soukromá poznámka",
"SEND": "Poslat",
"CREATE": "Přidat poznámku",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TWEET": "Tweet",
"TIP_FORMAT_ICON": "Zobrazit formátovaný textový editor",
"TIP_EMOJI_ICON": "Zobrazit výběr emoji",
"TIP_ATTACH_ICON": "Přiložit soubory",
@@ -195,7 +194,6 @@
},
"CONTEXT_MENU": {
"COPY": "Kopírovat",
"REPLY_TO": "Reply to this message",
"DELETE": "Vymazat",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -1,7 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search",
"REMOVE": "Odebrat"
"NOT_FOUND": "No emoji match your search"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Název",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Název",
"PLACEHOLDER": "Category name",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "The category name will be used in the public facing portal to categorize articles.",
"ERROR": "Name is required"
},
"SLUG": {
@@ -425,33 +425,6 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"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.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"HELP_CENTER": {
"TITLE": "Help Center",
"TITLE": "Help Center (Beta)",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,16 +41,6 @@
"STATUS": "Status",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Oprettet den:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,10 +204,6 @@
"PLACEHOLDER": "Skriv din besked her",
"ERROR": "Beskeden må ikke være tom"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send besked",
"CANCEL": "Annuller",
"SUCCESS_MESSAGE": "Besked sendt!",
@@ -347,17 +343,17 @@
"DESCRIPTION": "Sammenflet kontakter for at kombinere to profiler til én, herunder alle attributter og samtaler. I tilfælde af konflikter vil den primære kontakts attributter have forrang.",
"PRIMARY": {
"TITLE": "Primær kontakt",
"HELP_LABEL": "Skal slettes"
"HELP_LABEL": "Skal opbevares"
},
"PARENT": {
"CHILD": {
"TITLE": "Kontakt for sammenfletning",
"PLACEHOLDER": "Søg efter en kontakt",
"HELP_LABEL": "Skal opbevares"
"HELP_LABEL": "Skal slettes"
},
"SUMMARY": {
"TITLE": "Oversigt",
"DELETE_WARNING": "Kontakt af <strong>%{primaryContactName}</strong> vil blive slettet.",
"ATTRIBUTE_WARNING": "Kontaktoplysninger på <strong>%{primaryContactName}</strong> vil blive kopieret til <strong>%{parentContactName}</strong>."
"DELETE_WARNING": "Kontakt af <strong>%{childContactName}</strong> vil blive slettet.",
"ATTRIBUTE_WARNING": "Kontaktoplysninger på <strong>%{childContactName}</strong> vil blive kopieret til <strong>%{primaryContactName}</strong>."
},
"SEARCH": {
"ERROR": "FEJL_MEDDELELSE"
@@ -34,6 +34,7 @@
"ASSIGN_TO_ME": "Tildel til mig",
"TWILIO_WHATSAPP_CAN_REPLY": "Du kan kun svare på denne samtale ved hjælp af en skabelon besked på grund af",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 timers beskedvindue begrænsning",
"SELECT_A_TWEET_TO_REPLY": "Vælg venligst et tweet at svare på.",
"REPLYING_TO": "Du svarer til:",
"REMOVE_SELECTION": "Fjern Markering",
"DOWNLOAD": "Download",
@@ -46,7 +47,6 @@
"NO_RESPONSE": "Intet svar",
"RATING_TITLE": "Bedømmelse",
"FEEDBACK_TITLE": "Tilbagemelding",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Vis etiketter",
"HIDE_LABELS": "Skjul etiketter"
@@ -139,8 +139,7 @@
"PRIVATE_NOTE": "Privat Note",
"SEND": "Send",
"CREATE": "Tilføj Note",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TWEET": "Tweet",
"TIP_FORMAT_ICON": "Vis rig teksteditor",
"TIP_EMOJI_ICON": "Vis emoji-vælger",
"TIP_ATTACH_ICON": "Vedhæft filer",
@@ -195,7 +194,6 @@
},
"CONTEXT_MENU": {
"COPY": "Kopiér",
"REPLY_TO": "Reply to this message",
"DELETE": "Slet",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -1,7 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Søg emojis",
"NOT_FOUND": "Ingen emoji matcher din søgning",
"REMOVE": "Fjern"
"NOT_FOUND": "Ingen emoji matcher din søgning"
}
}
@@ -365,7 +365,7 @@
"NAME": {
"LABEL": "Navn",
"PLACEHOLDER": "Kategori navn",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.",
"ERROR": "Navn er påkrævet"
},
"SLUG": {
@@ -396,7 +396,7 @@
"NAME": {
"LABEL": "Navn",
"PLACEHOLDER": "Kategori navn",
"HELP_TEXT": "The category name and icon will be used in the public facing portal to categorize articles.",
"HELP_TEXT": "Kategorinavnet vil blive brugt i den offentlige vendende portal til at kategorisere artikler.",
"ERROR": "Navn er påkrævet"
},
"SLUG": {
@@ -425,33 +425,6 @@
"ERROR_MESSAGE": "Kan ikke slette kategori"
}
}
},
"UPGRADE_PAGE": {
"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.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Lær mere",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Oversigt",
"FACEBOOK_REAUTHORIZE": "Din Facebook-forbindelse er udløbet, tilslut venligst din Facebook-side igen for at fortsætte tjenesterne",
"HELP_CENTER": {
"TITLE": "Help Center",
"TITLE": "Hjælpecenter (Beta)",
"ALL_ARTICLES": "Alle Artikler",
"MY_ARTICLES": "Alle Artikler",
"DRAFT": "Kladde",
@@ -41,16 +41,6 @@
"STATUS": "Status",
"ORDER_BY": "Sortieren nach"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Erstellt",
"OLDEST": "Erstellt am:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Letzte Aktivität:",
"ACTIVE": "Letzte Aktivität"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Letzte Aktivität"
@@ -204,10 +204,6 @@
"PLACEHOLDER": "Schreiben Sie Ihre Nachricht hier",
"ERROR": "Nachricht darf nicht leer sein"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Nachricht senden",
"CANCEL": "Abbrechen",
"SUCCESS_MESSAGE": "Nachricht gesendet!",
@@ -347,17 +343,17 @@
"DESCRIPTION": "Kontakte zusammenführen, um zwei Profile zu einem zu kombinieren, einschließlich aller Attribute und Gespräche. Im Falle eines Konflikts haben die Attribute des Hauptkontakts Vorrang.",
"PRIMARY": {
"TITLE": "Hauptkontakt",
"HELP_LABEL": "Zu löschen"
"HELP_LABEL": "Zu erhalten"
},
"PARENT": {
"CHILD": {
"TITLE": "Kontakt zum Zusammenführen",
"PLACEHOLDER": "Nach Kontakt suchen",
"HELP_LABEL": "Zu erhalten"
"HELP_LABEL": "Zu löschen"
},
"SUMMARY": {
"TITLE": "Zusammenfassung",
"DELETE_WARNING": "Der Kontakt von <strong>%{primaryContactName}</strong> wird gelöscht.",
"ATTRIBUTE_WARNING": "Details von Kontakt <strong>%{primaryContactName}</strong> wird zu <strong>%{parentContactName}</strong> kopiert."
"DELETE_WARNING": "Der Kontakt von <strong>%{childContactName}</strong> wird gelöscht.",
"ATTRIBUTE_WARNING": "Details von Kontakt <strong>%{childContactName}</strong> wird zu <strong>%{primaryContactName}</strong> kopiert."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,6 +34,7 @@
"ASSIGN_TO_ME": "Mir zuweisen",
"TWILIO_WHATSAPP_CAN_REPLY": "Sie können auf diese Konversation nur mit einer Nachrichtenvorlage antworten wegen",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24-Stunden-Nachrichtenfenster-Beschränkung",
"SELECT_A_TWEET_TO_REPLY": "Bitte wählen Sie einen Tweet aus, auf welchen Sie antworten möchten.",
"REPLYING_TO": "Sie antworten auf:",
"REMOVE_SELECTION": "Auswahl entfernen",
"DOWNLOAD": "Herunterladen",
@@ -46,7 +47,6 @@
"NO_RESPONSE": "Keine Antwort",
"RATING_TITLE": "Bewertung",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Labels anzeigen",
"HIDE_LABELS": "Labels ausblenden"
@@ -139,8 +139,7 @@
"PRIVATE_NOTE": "Private Notiz",
"SEND": "Senden",
"CREATE": "Notiz hinzufügen",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TWEET": "Tweet",
"TIP_FORMAT_ICON": "Rich Text Editor anzeigen",
"TIP_EMOJI_ICON": "Emoji-Auswahl anzeigen",
"TIP_ATTACH_ICON": "Dateien anhängen",
@@ -195,7 +194,6 @@
},
"CONTEXT_MENU": {
"COPY": "Kopieren",
"REPLY_TO": "Reply to this message",
"DELETE": "Löschen",
"CREATE_A_CANNED_RESPONSE": "Zu vorgefertigten Antworten hinzufügen",
"TRANSLATE": "Übersetzen",
@@ -1,7 +1,6 @@
{
"EMOJI": {
"PLACEHOLDER": "Emojis durchsuchen",
"NOT_FOUND": "Kein Emoji entspricht deiner Suche",
"REMOVE": "Entfernen"
"NOT_FOUND": "Kein Emoji entspricht deiner Suche"
}
}

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