Merge branch 'release/1.12.0' into master

This commit is contained in:
Sojan 2021-01-18 14:17:44 +05:30
commit 1aaced3027
573 changed files with 6836 additions and 2471 deletions

View File

@ -18,7 +18,7 @@ FORCE_SSL=false
# true : default option, allows sign ups # true : default option, allows sign ups
# false : disables all the end points related to sign ups # false : disables all the end points related to sign ups
# api_only: disables the UI for signup, but you can create sign ups via the account apis # api_only: disables the UI for signup, but you can create sign ups via the account apis
ENABLE_ACCOUNT_SIGNUP=true ENABLE_ACCOUNT_SIGNUP=false
# Redis config # Redis config
REDIS_URL=redis://redis:6379 REDIS_URL=redis://redis:6379
@ -110,6 +110,12 @@ SLACK_CLIENT_SECRET=
## Mobile app env variables ## Mobile app env variables
IOS_APP_ID=6C953F3RX2.com.chatwoot.app IOS_APP_ID=6C953F3RX2.com.chatwoot.app
### Smart App Banner
# https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/PromotingAppswithAppBanners/PromotingAppswithAppBanners.html
# You can find your app-id in https://itunesconnect.apple.com
#IOS_APP_IDENTIFIER=1495796682
## Push Notification ## Push Notification
## generate a new key value here : https://d3v.one/vapid-key-generator/ ## generate a new key value here : https://d3v.one/vapid-key-generator/
# VAPID_PUBLIC_KEY= # VAPID_PUBLIC_KEY=

3
.gitignore vendored
View File

@ -60,3 +60,6 @@ package-lock.json
# cypress # cypress
test/cypress/videos/* test/cypress/videos/*
/config/master.key
/config/*.enc

View File

@ -11,6 +11,8 @@ Metrics/ClassLength:
Max: 125 Max: 125
Exclude: Exclude:
- 'app/models/conversation.rb' - 'app/models/conversation.rb'
- 'app/mailers/conversation_reply_mailer.rb'
- 'app/models/message.rb'
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 25 Max: 25
Style/Documentation: Style/Documentation:
@ -48,6 +50,7 @@ Rails/ApplicationController:
- 'app/controllers/dashboard_controller.rb' - 'app/controllers/dashboard_controller.rb'
- 'app/controllers/widget_tests_controller.rb' - 'app/controllers/widget_tests_controller.rb'
- 'app/controllers/widgets_controller.rb' - 'app/controllers/widgets_controller.rb'
- 'app/controllers/platform_controller.rb'
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
EnforcedStyle: compact EnforcedStyle: compact
Exclude: Exclude:

View File

@ -6,13 +6,6 @@
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 1
# Configuration parameters: EnforcedStyle.
# SupportedStyles: native, lf, crlf
Layout/EndOfLine:
Exclude:
- 'deploy/after_restart.rb'
# Offense count: 1 # Offense count: 1
Lint/DuplicateMethods: Lint/DuplicateMethods:
Exclude: Exclude:

View File

@ -29,6 +29,8 @@ gem 'flag_shih_tzu'
gem 'haikunator' gem 'haikunator'
# Template parsing safetly # Template parsing safetly
gem 'liquid' gem 'liquid'
# Parse Markdown to HTML
gem 'redcarpet'
##-- for active storage --## ##-- for active storage --##
gem 'aws-sdk-s3', require: false gem 'aws-sdk-s3', require: false
@ -85,6 +87,8 @@ gem 'sentry-raven'
##-- background job processing --## ##-- background job processing --##
gem 'sidekiq' gem 'sidekiq'
# We want cron jobs
gem 'sidekiq-cron'
##-- Push notification service --## ##-- Push notification service --##
gem 'fcm' gem 'fcm'
@ -96,6 +100,9 @@ gem 'geocoder'
# to parse maxmind db # to parse maxmind db
gem 'maxminddb' gem 'maxminddb'
# to create db triggers
gem 'hairtrigger'
group :development do group :development do
gem 'annotate' gem 'annotate'
gem 'bullet' gem 'bullet'

View File

@ -115,13 +115,14 @@ GEM
descendants_tracker (~> 0.0.4) descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0) ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
azure-storage-blob (2.0.0) azure-storage-blob (2.0.1)
azure-storage-common (~> 2.0) azure-storage-common (~> 2.0)
nokogiri (~> 1.10.4) nokogiri (~> 1.11.0.rc2)
azure-storage-common (2.0.1) azure-storage-common (2.0.2)
faraday (~> 1.0) faraday (~> 1.0)
faraday_middleware (~> 1.0.0.rc1) faraday_middleware (~> 1.0.0.rc1)
nokogiri (~> 1.10.4) net-http-persistent (~> 4.0)
nokogiri (~> 1.11.0.rc2)
barnes (0.0.8) barnes (0.0.8)
multi_json (~> 1) multi_json (~> 1)
statsd-ruby (~> 1.1) statsd-ruby (~> 1.1)
@ -181,6 +182,8 @@ GEM
railties (>= 3.2) railties (>= 3.2)
equalizer (0.0.11) equalizer (0.0.11)
erubi (1.9.0) erubi (1.9.0)
et-orbi (1.2.4)
tzinfo
execjs (2.7.0) execjs (2.7.0)
facebook-messenger (1.5.0) facebook-messenger (1.5.0)
httparty (~> 0.13, >= 0.13.7) httparty (~> 0.13, >= 0.13.7)
@ -198,9 +201,12 @@ GEM
faraday (~> 1.0) faraday (~> 1.0)
fcm (1.0.2) fcm (1.0.2)
faraday (~> 1.0.0) faraday (~> 1.0.0)
ffi (1.13.1) ffi (1.14.2)
flag_shih_tzu (0.3.23) flag_shih_tzu (0.3.23)
foreman (0.87.2) foreman (0.87.2)
fugit (1.4.1)
et-orbi (~> 1.1, >= 1.1.8)
raabro (~> 1.4)
geocoder (1.6.3) geocoder (1.6.3)
gli (2.19.2) gli (2.19.2)
globalid (0.4.2) globalid (0.4.2)
@ -236,6 +242,10 @@ GEM
groupdate (5.1.0) groupdate (5.1.0)
activesupport (>= 5) activesupport (>= 5)
haikunator (1.1.0) haikunator (1.1.0)
hairtrigger (0.2.23)
activerecord (>= 5.0, < 7)
ruby2ruby (~> 2.4)
ruby_parser (~> 3.10)
hana (1.3.6) hana (1.3.6)
hashdiff (1.0.1) hashdiff (1.0.1)
hashie (4.1.0) hashie (4.1.0)
@ -281,7 +291,7 @@ GEM
letter_opener (1.7.0) letter_opener (1.7.0)
launchy (~> 2.2) launchy (~> 2.2)
liquid (4.0.3) liquid (4.0.3)
listen (3.2.1) listen (3.3.3)
rb-fsevent (~> 0.10, >= 0.10.3) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10) rb-inotify (~> 0.9, >= 0.9.10)
loofah (2.7.0) loofah (2.7.0)
@ -300,7 +310,7 @@ GEM
mimemagic (0.3.5) mimemagic (0.3.5)
mini_magick (4.10.1) mini_magick (4.10.1)
mini_mime (1.0.2) mini_mime (1.0.2)
mini_portile2 (2.4.0) mini_portile2 (2.5.0)
minitest (5.14.2) minitest (5.14.2)
momentjs-rails (2.20.1) momentjs-rails (2.20.1)
railties (>= 3.1) railties (>= 3.1)
@ -308,10 +318,13 @@ GEM
multi_json (1.15.0) multi_json (1.15.0)
multi_xml (0.6.0) multi_xml (0.6.0)
multipart-post (2.1.1) multipart-post (2.1.1)
net-http-persistent (4.0.0)
connection_pool (~> 2.2)
netrc (0.11.0) netrc (0.11.0)
nio4r (2.5.4) nio4r (2.5.4)
nokogiri (1.10.10) nokogiri (1.11.0)
mini_portile2 (~> 2.4.0) mini_portile2 (~> 2.5.0)
racc (~> 1.4)
oauth (0.5.4) oauth (0.5.4)
orm_adapter (0.5.0) orm_adapter (0.5.0)
os (1.1.1) os (1.1.1)
@ -329,6 +342,8 @@ GEM
nio4r (~> 2.0) nio4r (~> 2.0)
pundit (2.1.0) pundit (2.1.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0)
racc (1.5.2)
rack (2.2.3) rack (2.2.3)
rack-cache (1.12.0) rack-cache (1.12.0)
rack (>= 0.4) rack (>= 0.4)
@ -369,6 +384,7 @@ GEM
rb-fsevent (0.10.4) rb-fsevent (0.10.4)
rb-inotify (0.10.1) rb-inotify (0.10.1)
ffi (~> 1.0) ffi (~> 1.0)
redcarpet (3.5.1)
redis (4.2.1) redis (4.2.1)
redis-namespace (1.8.0) redis-namespace (1.8.0)
redis (>= 3.0.4) redis (>= 3.0.4)
@ -422,13 +438,19 @@ GEM
parser (>= 2.7.1.4) parser (>= 2.7.1.4)
rubocop-performance (1.7.1) rubocop-performance (1.7.1)
rubocop (>= 0.82.0) rubocop (>= 0.82.0)
rubocop-rails (2.7.1) rubocop-rails (2.8.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
rack (>= 1.1) rack (>= 1.1)
rubocop (>= 0.87.0) rubocop (>= 0.87.0)
rubocop-rspec (1.43.2) rubocop-rspec (1.43.2)
rubocop (~> 0.87) rubocop (~> 0.87)
ruby-progressbar (1.10.1) ruby-progressbar (1.10.1)
ruby2ruby (2.4.4)
ruby_parser (~> 3.1)
sexp_processor (~> 4.6)
ruby_parser (3.15.0)
rubocop (>= 0.87.0)
sexp_processor (~> 4.9)
safe_yaml (1.0.5) safe_yaml (1.0.5)
sass (3.7.4) sass (3.7.4)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
@ -454,12 +476,16 @@ GEM
semantic_range (2.3.0) semantic_range (2.3.0)
sentry-raven (3.0.3) sentry-raven (3.0.3)
faraday (>= 1.0) faraday (>= 1.0)
sexp_processor (4.15.1)
shoulda-matchers (4.4.1) shoulda-matchers (4.4.1)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
sidekiq (6.1.1) sidekiq (6.1.1)
connection_pool (>= 2.2.2) connection_pool (>= 2.2.2)
rack (~> 2.0) rack (~> 2.0)
redis (>= 4.2.0) redis (>= 4.2.0)
sidekiq-cron (1.2.0)
fugit (~> 1.1)
sidekiq (>= 4.2.1)
signet (0.14.0) signet (0.14.0)
addressable (~> 2.3) addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0) faraday (>= 0.17.3, < 2.0)
@ -506,7 +532,7 @@ GEM
nokogiri (>= 1.6, < 2.0) nokogiri (>= 1.6, < 2.0)
twitty (0.1.1) twitty (0.1.1)
oauth oauth
tzinfo (1.2.7) tzinfo (1.2.8)
thread_safe (~> 0.1) thread_safe (~> 0.1)
tzinfo-data (1.2020.1) tzinfo-data (1.2020.1)
tzinfo (>= 1.0.0) tzinfo (>= 1.0.0)
@ -549,7 +575,7 @@ GEM
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5) websocket-extensions (0.1.5)
wisper (2.0.0) wisper (2.0.0)
zeitwerk (2.4.0) zeitwerk (2.4.1)
PLATFORMS PLATFORMS
ruby ruby
@ -584,6 +610,7 @@ DEPENDENCIES
google-cloud-storage google-cloud-storage
groupdate groupdate
haikunator haikunator
hairtrigger
hashie hashie
jbuilder jbuilder
json_refs! json_refs!
@ -602,6 +629,7 @@ DEPENDENCIES
pundit pundit
rack-cors rack-cors
rails rails
redcarpet
redis redis
redis-namespace redis-namespace
redis-rack-cache redis-rack-cache
@ -618,6 +646,7 @@ DEPENDENCIES
sentry-raven sentry-raven
shoulda-matchers shoulda-matchers
sidekiq sidekiq
sidekiq-cron
simplecov (= 0.17.1) simplecov (= 0.17.1)
slack-ruby-client slack-ruby-client
spring spring

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2017-2020 ThoughtWoot Inc. Copyright (c) 2017-2021 Chatwoot Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -64,7 +64,7 @@ Detailed documentation is available at [www.chatwoot.com/help-center](https://ww
### Translation process ### Translation process
The translation process for Chatwoot web and mobile app is managed at [https://translate.chatwoot.com](https://translate.chatwoot.com) using Crowdin. Please read the [translation guide](https://www.chatwoot/docs/contributing/translating-chatwoot-to-your-language) for contributing to Chatwoot. The translation process for Chatwoot web and mobile app is managed at [https://translate.chatwoot.com](https://translate.chatwoot.com) using Crowdin. Please read the [translation guide](https://www.chatwoot.com/docs/contributing/translating-chatwoot-to-your-language) for contributing to Chatwoot.
--- ---
@ -98,4 +98,4 @@ Thanks goes to all these [wonderful people](https://www.chatwoot.com/docs/contri
<a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a> <a href="https://github.com/chatwoot/chatwoot/graphs/contributors"><img src="https://opencollective.com/chatwoot/contributors.svg?width=890&button=false" /></a>
*Chatwoot* &copy; 2017-2020, ThoughtWoot Inc - Released under the MIT License. *Chatwoot* &copy; 2017-2021, Chatwoot Inc - Released under the MIT License.

View File

@ -11,7 +11,10 @@
"rails", "rails",
"vue" "vue"
], ],
"success_url": "/app/login", "success_url": "/",
"scripts": {
"postdeploy": "bundle exec rake db:seed"
},
"env": { "env": {
"SECRET_TOKEN": { "SECRET_TOKEN": {
"description": "A secret key for verifying the integrity of signed cookies.", "description": "A secret key for verifying the integrity of signed cookies.",

View File

@ -2,7 +2,7 @@
class AccountBuilder class AccountBuilder
include CustomExceptions::Account include CustomExceptions::Account
pattr_initialize [:account_name!, :email!, :confirmed!, :user] pattr_initialize [:account_name!, :email!, :confirmed!, :user, :user_full_name, :user_password]
def perform def perform
if @user.nil? if @user.nil?
@ -15,7 +15,6 @@ class AccountBuilder
end end
[@user, @account] [@user, @account]
rescue StandardError => e rescue StandardError => e
@account&.destroy
puts e.inspect puts e.inspect
raise e raise e
end end
@ -27,7 +26,7 @@ class AccountBuilder
if address.valid? # && !address.disposable? if address.valid? # && !address.disposable?
true true
else else
raise InvalidEmail.new(valid: address.valid?) # , disposable: address.disposable?}) raise InvalidEmail.new(valid: address.valid?)
end end
end end
@ -61,18 +60,13 @@ class AccountBuilder
) )
end end
def email_to_name(email)
name = email[/[^@]+/]
name.split('.').map(&:capitalize).join(' ')
end
def create_user def create_user
password = SecureRandom.alphanumeric(12) password = user_password || SecureRandom.alphanumeric(12)
@user = User.new(email: @email, @user = User.new(email: @email,
password: password, password: password,
password_confirmation: password, password_confirmation: password,
name: email_to_name(@email)) name: @user_full_name)
@user.confirm if @confirmed @user.confirm if @confirmed
@user.save! @user.save!
end end

View File

@ -5,7 +5,7 @@ class ContactBuilder
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id) contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
return contact_inbox if contact_inbox return contact_inbox if contact_inbox
build_contact build_contact_inbox
end end
private private
@ -26,16 +26,29 @@ class ContactBuilder
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url] ::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
end end
def build_contact def create_contact
ActiveRecord::Base.transaction do account.contacts.create!(
contact = account.contacts.create!( name: contact_attributes[:name],
name: contact_attributes[:name], phone_number: contact_attributes[:phone_number],
phone_number: contact_attributes[:phone_number], email: contact_attributes[:email],
email: contact_attributes[:email], identifier: contact_attributes[:identifier],
identifier: contact_attributes[:identifier], additional_attributes: contact_attributes[:additional_attributes]
additional_attributes: contact_attributes[:additional_attributes] )
) end
def find_contact
contact = nil
contact = account.contacts.find_by(identifier: contact_attributes[:identifier]) if contact_attributes[:identifier].present?
contact ||= account.contacts.find_by(email: contact_attributes[:email]) if contact_attributes[:email].present?
contact
end
def build_contact_inbox
ActiveRecord::Base.transaction do
contact = find_contact || create_contact
contact_inbox = create_contact_inbox(contact) contact_inbox = create_contact_inbox(contact)
update_contact_avatar(contact) update_contact_avatar(contact)
contact_inbox contact_inbox

View File

@ -0,0 +1,13 @@
class Api::V1::Accounts::Contacts::LabelsController < Api::V1::Accounts::BaseController
include LabelConcern
private
def model
@model ||= Current.account.contacts.find(permitted_params[:contact_id])
end
def permitted_params
params.permit(:contact_id, labels: [])
end
end

View File

@ -1,11 +1,13 @@
class Api::V1::Accounts::Conversations::LabelsController < Api::V1::Accounts::Conversations::BaseController class Api::V1::Accounts::Conversations::LabelsController < Api::V1::Accounts::Conversations::BaseController
def create include LabelConcern
@conversation.update_labels(params[:labels])
@labels = @conversation.label_list private
def model
@model ||= @conversation
end end
# all labels of the current conversation def permitted_params
def index params.permit(:conversation_id, labels: [])
@labels = @conversation.label_list
end end
end end

View File

@ -11,9 +11,24 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
render_could_not_create_error(e.message) render_could_not_create_error(e.message)
end end
def destroy
ActiveRecord::Base.transaction do
message.update!(content: I18n.t('conversations.messages.deleted'), deleted: true)
message.attachments.destroy_all
end
end
private private
def message
@message ||= @conversation.messages.find(permitted_params[:id])
end
def message_finder def message_finder
@message_finder ||= MessageFinder.new(@conversation, params) @message_finder ||= MessageFinder.new(@conversation, params)
end end
def permitted_params
params.permit(:id)
end
end end

View File

@ -86,7 +86,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
account_id: Current.account.id, account_id: Current.account.id,
inbox_id: @contact_inbox.inbox_id, inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id, contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id contact_inbox_id: @contact_inbox.id,
additional_attributes: params[:additional_attributes]
} }
end end

View File

@ -3,10 +3,12 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
before_action :fetch_notification, only: [:update] before_action :fetch_notification, only: [:update]
before_action :set_primary_actor, only: [:read_all] before_action :set_primary_actor, only: [:read_all]
before_action :set_current_page, only: [:index]
def index def index
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count @unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
@notifications = current_user.notifications.where(account_id: current_account.id).page params[:page] @count = notifications.count
@notifications = notifications.page @current_page
end end
def read_all def read_all
@ -25,6 +27,11 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
render json: @notification render json: @notification
end end
def unread_count
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
render json: @unread_count
end
private private
def set_primary_actor def set_primary_actor
@ -37,4 +44,12 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
def fetch_notification def fetch_notification
@notification = current_user.notifications.find(params[:id]) @notification = current_user.notifications.find(params[:id])
end end
def set_current_page
@current_page = params[:page] || 1
end
def notifications
@notifications ||= current_user.notifications.where(account_id: current_account.id)
end
end end

View File

@ -0,0 +1,24 @@
class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseController
before_action :fetch_team
before_action :check_authorization
def index
@team_members = @team.team_members.map(&:user)
end
def create
record = @team.team_members.find_or_create_by(user_id: params[:user_id])
@team_member = record.user
end
def destroy
@team.team_members.find_by(user_id: params[:user_id])&.destroy
head :ok
end
private
def fetch_team
@team = Current.account.teams.find(params[:team_id])
end
end

View File

@ -0,0 +1,34 @@
class Api::V1::Accounts::TeamsController < Api::V1::Accounts::BaseController
before_action :fetch_team, only: [:show, :update, :destroy]
before_action :check_authorization
def index
@teams = Current.account.teams
end
def show; end
def create
@team = Current.account.teams.new(team_params)
@team.save!
end
def update
@team.update!(team_params)
end
def destroy
@team.destroy
head :ok
end
private
def fetch_team
@team = Current.account.teams.find(params[:id])
end
def team_params
params.require(:team).permit(:name, :description, :allow_auto_assign)
end
end

View File

@ -16,6 +16,7 @@ class Api::V1::AccountsController < Api::BaseController
def create def create
@user, @account = AccountBuilder.new( @user, @account = AccountBuilder.new(
account_name: account_params[:account_name], account_name: account_params[:account_name],
user_full_name: account_params[:user_full_name],
email: account_params[:email], email: account_params[:email],
confirmed: confirmed?, confirmed: confirmed?,
user: current_user user: current_user
@ -29,6 +30,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def show def show
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
render 'api/v1/accounts/show.json' render 'api/v1/accounts/show.json'
end end
@ -54,7 +56,7 @@ class Api::V1::AccountsController < Api::BaseController
end end
def account_params def account_params
params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :auto_resolve_duration) params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :auto_resolve_duration, :user_full_name)
end end
def check_signup_enabled def check_signup_enabled

View File

@ -23,7 +23,8 @@ class Api::V1::ProfilesController < Api::BaseController
:password, :password,
:password_confirmation, :password_confirmation,
:avatar, :avatar,
:availability :availability,
ui_settings: {}
) )
end end
end end

View File

@ -7,7 +7,12 @@ class Api::V1::Widget::BaseController < ApplicationController
private private
def conversations def conversations
@conversations = @contact_inbox.conversations.where(inbox_id: auth_token_params[:inbox_id]) if @contact_inbox.hmac_verified?
verified_contact_inbox_ids = @contact.contact_inboxes.where(inbox_id: auth_token_params[:inbox_id], hmac_verified: true).map(&:id)
@conversations = @contact.conversations.where(contact_inbox_id: verified_contact_inbox_ids)
else
@conversations = @contact_inbox.conversations.where(inbox_id: auth_token_params[:inbox_id])
end
end end
def conversation def conversation

View File

@ -1,5 +1,6 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
def update def update
process_hmac
contact_identify_action = ContactIdentifyAction.new( contact_identify_action = ContactIdentifyAction.new(
contact: @contact, contact: @contact,
params: permitted_params.to_h.deep_symbolize_keys params: permitted_params.to_h.deep_symbolize_keys
@ -9,7 +10,22 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
private private
def process_hmac
return if params[:identifier_hash].blank?
raise StandardError, 'HMAC failed: Invalid Identifer Hash Provided' unless valid_hmac?
@contact_inbox.update(hmac_verified: true)
end
def valid_hmac?
params[:identifier_hash] == OpenSSL::HMAC.hexdigest(
'sha256',
@web_widget.hmac_token,
params[:identifier].to_s
)
end
def permitted_params def permitted_params
params.permit(:website_token, :identifier, :email, :name, :avatar_url, custom_attributes: {}) params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, custom_attributes: {})
end end
end end

View File

@ -54,6 +54,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
end end
def conversation_params def conversation_params
# FIXME: typo referrer in additional attributes
# will probably require a migration.
{ {
account_id: inbox.account_id, account_id: inbox.account_id,
inbox_id: inbox.id, inbox_id: inbox.id,

View File

@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base
render_unauthorized('You are not authorized to do this action') render_unauthorized('You are not authorized to do this action')
ensure ensure
# to address the thread variable leak issues in Puma/Thin webserver # to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil Current.reset
end end
def set_current_user def set_current_user

View File

@ -0,0 +1,10 @@
module LabelConcern
def create
model.update_labels(permitted_params[:labels])
@labels = model.label_list
end
def index
@labels = model.label_list
end
end

View File

@ -1,5 +1,10 @@
class DashboardController < ActionController::Base class DashboardController < ActionController::Base
include SwitchLocale
before_action :set_global_config before_action :set_global_config
around_action :switch_locale
before_action :ensure_installation_onboarding, only: [:index]
layout 'vueapp' layout 'vueapp'
def index; end def index; end
@ -20,4 +25,8 @@ class DashboardController < ActionController::Base
APP_VERSION: Chatwoot.config[:version] APP_VERSION: Chatwoot.config[:version]
) )
end end
def ensure_installation_onboarding
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
end
end end

View File

@ -0,0 +1,36 @@
class Installation::OnboardingController < ApplicationController
before_action :ensure_installation_onboarding
def index; end
def create
begin
AccountBuilder.new(
account_name: onboarding_params.dig(:user, :company),
user_full_name: onboarding_params.dig(:user, :name),
email: onboarding_params.dig(:user, :email),
user_password: params.dig(:user, :password),
confirmed: true
).perform
rescue StandardError => e
redirect_to '/', flash: { error: e.message } and return
end
finish_onboarding
redirect_to '/'
end
private
def onboarding_params
params.permit(:subscribe_to_updates, user: [:name, :company, :email])
end
def finish_onboarding
::Redis::Alfred.delete(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
ChatwootHub.register_instance(onboarding_params) if onboarding_params[:subscribe_to_updates]
end
def ensure_installation_onboarding
redirect_to '/' unless ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
end
end

View File

@ -0,0 +1,29 @@
class Platform::Api::V1::AccountUsersController < PlatformController
before_action :set_resource
before_action :validate_platform_app_permissible
def index
render json: @resource.account_users
end
def create
@account_user = @resource.account_users.find_or_initialize_by(user_id: account_user_params[:user_id])
@account_user.update!(account_user_params)
render json: @account_user
end
def destroy
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy
head :ok
end
private
def set_resource
@resource = Account.find(params[:account_id])
end
def account_user_params
params.permit(:user_id, :role)
end
end

View File

@ -0,0 +1,32 @@
class Platform::Api::V1::AccountsController < PlatformController
def create
@resource = Account.new(account_params)
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
render json: @resource
end
def show
render json: @resource
end
def update
@resource.update!(account_params)
render json: @resource
end
def destroy
# TODO: obfusicate account
head :ok
end
private
def set_resource
@resource = Account.find(params[:id])
end
def account_params
params.permit(:name)
end
end

View File

@ -0,0 +1,43 @@
class Platform::Api::V1::UsersController < PlatformController
# ref: https://stackoverflow.com/a/45190318/939299
# set resource is called for other actions already in platform controller
# we want to add login to that chain as well
before_action(only: [:login]) { set_resource }
before_action(only: [:login]) { validate_platform_app_permissible }
def create
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
@resource.confirm
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
render json: @resource
end
def login
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{@resource.email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
end
def show
render json: @resource
end
def update
@resource.update!(user_params)
render json: @resource
end
def destroy
# TODO: obfusicate user
head :ok
end
private
def set_resource
@resource = User.find(params[:id])
end
def user_params
params.permit(:name, :email, :password)
end
end

View File

@ -0,0 +1,37 @@
class PlatformController < ActionController::Base
protect_from_forgery with: :null_session
before_action :ensure_access_token
before_action :set_platform_app
before_action :set_resource, only: [:update, :show, :destroy]
before_action :validate_platform_app_permissible, only: [:update, :show, :destroy]
def show; end
def update; end
def destroy; end
private
def ensure_access_token
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
@access_token = AccessToken.find_by(token: token) if token.present?
end
def set_platform_app
@platform_app = @access_token.owner if @access_token && @access_token.owner.is_a?(PlatformApp)
render json: { error: 'Invalid access_token' }, status: :unauthorized if @platform_app.blank?
end
def set_resource
# set @resource in your controller
raise 'Overwrite this method your controller'
end
def validate_platform_app_permissible
return if @platform_app.platform_app_permissibles.find_by(permissible: @resource)
render json: { error: 'Non permissible resource' }, status: :unauthorized
end
end

View File

@ -0,0 +1,46 @@
class SuperAdmin::InstallationConfigsController < SuperAdmin::ApplicationController
# Overwrite any of the RESTful controller actions to implement custom behavior
# For example, you may want to send an email after a foo is updated.
#
# def update
# super
# send_foo_updated_email(requested_resource)
# end
# Override this method to specify custom lookup behavior.
# This will be used to set the resource for the `show`, `edit`, and `update`
# actions.
#
# def find_resource(param)
# Foo.find_by!(slug: param)
# end
# The result of this lookup will be available as `requested_resource`
# Override this if you have certain roles that require a subset
# this will be used to set the records shown on the `index` action.
#
def scoped_resource
resource_class.editable
end
# Override `resource_params` if you want to transform the submitted
# data before it's persisted. For example, the following would turn all
# empty values into nil values. It uses other APIs such as `resource_class`
# and `dashboard`:
#
# def resource_params
# params.require(resource_class.model_name.param_key).
# permit(dashboard.permitted_attributes).
# transform_values { |value| value == "" ? nil : value }
# end
def resource_params
params.require(:installation_config)
.permit(:name, :value)
.transform_values { |value| value == '' ? nil : value }.merge(locked: false)
end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end

View File

@ -0,0 +1,66 @@
require 'administrate/base_dashboard'
class InstallationConfigDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
id: Field::Number,
name: Field::String,
value: SerializedField,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
id
name
value
created_at
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
id
name
value
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
name
value
].freeze
# COLLECTION_FILTERS
# a hash that defines filters that can be used while searching via the search
# field of the dashboard.
#
# For example to add an option to search for open resources by typing "open:"
# in the search field:
#
# COLLECTION_FILTERS = {
# open: ->(resources) { resources.where(open: true) }
# }.freeze
COLLECTION_FILTERS = {}.freeze
# Overwrite this method to customize how installation configs are displayed
# across all pages of the admin dashboard.
#
# def display_resource(installation_config)
# "InstallationConfig ##{installation_config.id}"
# end
end

View File

@ -0,0 +1,15 @@
require 'administrate/field/base'
class SerializedField < Administrate::Field::Base
def to_s
hash? ? data.as_json : data.to_s
end
def hash?
data.is_a? Hash
end
def array?
data.is_a? Array
end
end

View File

@ -26,7 +26,8 @@ export default {
const fetchPromise = new Promise((resolve, reject) => { const fetchPromise = new Promise((resolve, reject) => {
axios axios
.post(urlData.url, { .post(urlData.url, {
account_name: creds.name, account_name: creds.accountName.trim(),
user_full_name: creds.fullName.trim(),
email: creds.email, email: creds.email,
}) })
.then(response => { .then(response => {
@ -139,6 +140,12 @@ export default {
return axios.put(endPoints('profileUpdate').url, formData); return axios.put(endPoints('profileUpdate').url, formData);
}, },
updateUISettings({ uiSettings }) {
return axios.put(endPoints('profileUpdate').url, {
profile: { ui_settings: uiSettings },
});
},
updateAvailability({ availability }) { updateAvailability({ availability }) {
return axios.put(endPoints('profileUpdate').url, { return axios.put(endPoints('profileUpdate').url, {
profile: { availability }, profile: { availability },

View File

@ -7,11 +7,26 @@ class MessageApi extends ApiClient {
super('conversations', { accountScoped: true }); super('conversations', { accountScoped: true });
} }
create({ conversationId, message, private: isPrivate, contentAttributes }) { create({
return axios.post(`${this.url}/${conversationId}/messages`, { conversationId,
content: message, message,
private: isPrivate, private: isPrivate,
content_attributes: contentAttributes, contentAttributes,
echo_id: echoId,
file,
}) {
const formData = new FormData();
if (file) formData.append('attachments[]', file, file.name);
if (message) formData.append('content', message);
if (contentAttributes)
formData.append('content_attributes', JSON.stringify(contentAttributes));
formData.append('private', isPrivate);
formData.append('echo_id', echoId);
return axios({
method: 'post',
url: `${this.url}/${conversationId}/messages`,
data: formData,
}); });
} }
@ -20,17 +35,6 @@ class MessageApi extends ApiClient {
params: { before }, params: { before },
}); });
} }
sendAttachment([conversationId, { file, isPrivate = false }]) {
const formData = new FormData();
formData.append('attachments[]', file, file.name);
formData.append('private', isPrivate);
return axios({
method: 'post',
url: `${this.url}/${conversationId}/messages`,
data: formData,
});
}
} }
export default new MessageApi(); export default new MessageApi();

View File

@ -1,4 +1,3 @@
.button { .button {
font-family: $body-font-family; font-family: $body-font-family;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
@ -35,6 +34,7 @@ code {
&.hljs { &.hljs {
background: $color-background; background: $color-background;
border-radius: var(--border-radius-large);
padding: $space-two; padding: $space-two;
} }
} }

View File

@ -236,18 +236,18 @@ $breadcrumbs-item-slash: true;
// 11. Button // 11. Button
// ---------- // ----------
$button-padding: $space-one $space-normal; $button-padding: var(--space-one) var(--space-slab);
$button-margin: 0 0 $global-margin 0; $button-margin: 0 0 $global-margin 0;
$button-fill: solid; $button-fill: solid;
$button-background: $primary-color; $button-background: $primary-color;
$button-background-hover: scale-color($button-background, $lightness: -15%); $button-background-hover: scale-color($button-background, $lightness: -15%);
$button-color: $white; $button-color: $white;
$button-color-alt: $white; $button-color-alt: $white;
$button-radius: $global-radius; $button-radius: var(--border-radius-normal);
$button-sizes: (tiny: $font-size-micro, $button-sizes: (tiny: var(--font-size-nano),
small: $font-size-mini, small: var(--font-size-mini),
default: $font-size-default, default: var(--font-size-small),
large: $font-size-large); large: var(--font-size-medium));
$button-palette: $foundation-palette; $button-palette: $foundation-palette;
$button-opacity-disabled: 0.25; $button-opacity-disabled: 0.25;
$button-background-hover-lightness: -20%; $button-background-hover-lightness: -20%;

View File

@ -23,7 +23,7 @@
@import 'views/settings/inbox'; @import 'views/settings/inbox';
@import 'views/settings/channel'; @import 'views/settings/channel';
@import 'views/settings/integrations'; @import 'views/settings/integrations';
@import 'views/signup';
@import 'plugins/multiselect'; @import 'plugins/multiselect';
@import 'plugins/dropdown'; @import 'plugins/dropdown';
@import '@chatwoot/prosemirror-schema/src/woot-editor.css';

View File

@ -3,10 +3,12 @@
@import 'shared/assets/stylesheets/spacing'; @import 'shared/assets/stylesheets/spacing';
@import 'shared/assets/stylesheets/font-size'; @import 'shared/assets/stylesheets/font-size';
@import 'shared/assets/stylesheets/font-weights'; @import 'shared/assets/stylesheets/font-weights';
@import 'shared/assets/stylesheets/shadows';
@import 'shared/assets/stylesheets/border-radius';
@import 'variables'; @import 'variables';
@import '~spinkit/scss/spinners/7-three-bounce'; @import '~spinkit/scss/spinners/7-three-bounce';
@import '~ionicons/scss/ionicons'; @import '~shared/assets/stylesheets/ionicons';
@import 'mixins'; @import 'mixins';
@import 'foundation-settings'; @import 'foundation-settings';

View File

@ -1,15 +1,36 @@
@import '../variables'; @import '../variables';
.superadmin-body { .superadmin-body {
background: $color-background; background: var(--color-background);
.hero--title {
font-size: var(--font-size-mega);
font-weight: var(--font-weight-light);
margin-top: var(--space-large);
}
} }
.alert-box { .alert-box {
background-color: $alert-color; background-color: var(--r-500);
border-radius: 5px; border-radius: 5px;
color: $color-white; color: var(--color-white);
font-size: 14px; font-size: 14px;
margin-bottom: 14px; margin-bottom: 14px;
padding: 10px; padding: 10px;
text-align: center; text-align: center;
} }
.update-subscription--checkbox {
display: flex;
input {
line-height: 1.5;
margin-right: var(--space-one);
}
div {
font-size: var(--font-size-small);
line-height: 1.5;
margin-bottom: var(--space-normal);
}
}

View File

@ -1,3 +1,3 @@
@import 'shared/assets/fonts/inter'; @import 'shared/assets/fonts/inter';
@import '../variables'; @import '../variables';
@import '~ionicons/scss/ionicons'; @import '~shared/assets/stylesheets/ionicons';

View File

@ -1,94 +0,0 @@
.signup {
// margin-top: $space-larger*1.2;
.signup--hero {
margin-bottom: $space-larger * 1.5;
.hero--logo {
width: 180px;
}
.hero--title {
margin-top: $space-large;
font-weight: $font-weight-light;
}
.hero--sub {
font-size: $font-size-medium;
color: $medium-gray;
}
}
.signup--features {
list-style-type: none;
font-size: $font-size-medium;
> li {
padding: $space-slab;
> i {
margin-right: $space-two;
font-size: $font-size-large;
&.beer {
color: #dfb63b;
}
&.report {
color: #2196f3;
}
&.canned {
color: #1cad22;
}
&.uptime {
color: #a753b5;
}
&.secure {
color: #607d8b;
}
}
}
}
.signup--box {
@include elegant-card;
padding: $space-large;
label {
font-size: $font-size-default;
color: $color-gray;
input {
padding: $space-slab;
height: $space-larger;
font-size: $font-size-default;
}
.error {
font-size: $font-size-small
}
}
}
.sigin--footer {
padding: $space-medium;
font-size: $font-size-default;
> a {
font-weight: $font-weight-bold;
}
}
.accept--terms {
font-size: $font-size-mini;
text-align: center;
@include margin($zero);
a {
font-size: $font-size-mini;
}
}
}

View File

@ -1,6 +1,21 @@
.button { .button {
margin-bottom: 0; margin-bottom: 0;
&.button--emoji {
background: var(--b-50);
border: 1px solid var(--color-border-light);
border-radius: var(--border-radius-large);
font-size: var(--font-size-small);
margin-right: var(--space-small);
padding: var(--space-small);
&:hover {
background: var(--b-200);
}
}
&.icon { &.icon {
padding-left: $space-normal; padding-left: $space-normal;
padding-right: $space-normal; padding-right: $space-normal;

View File

@ -72,7 +72,7 @@ $resolve-button-width: 13.2rem;
.button.resolve--button { .button.resolve--button {
@include flex-align($x: center, $y: middle); @include flex-align($x: center, $y: middle);
font-size: var(--font-size-default);
width: $resolve-button-width; width: $resolve-button-width;
>.icon { >.icon {

View File

@ -3,7 +3,7 @@
@include margin($zero); @include margin($zero);
background: $color-woot; background: $color-woot;
border-radius: $space-one; border-radius: $space-one;
color: $color-white; color: var(--white);
font-size: $font-size-small; font-size: $font-size-small;
font-weight: $font-weight-normal; font-weight: $font-weight-normal;
position: relative; position: relative;
@ -11,20 +11,12 @@
.message-text__wrap { .message-text__wrap {
position: relative; position: relative;
.link { .link {
color: $color-white; color: var(--white);
text-decoration: underline; text-decoration: underline;
} }
} }
.message-text {
&::after {
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
display: inline;
}
}
.image { .image {
cursor: pointer; cursor: pointer;
position: relative; position: relative;
@ -95,17 +87,15 @@
} }
} }
.content-box { .content-box {
text-align: center; text-align: center;
} }
} }
.conversation-wrap { .conversation-wrap {
@include background-gray;
@include margin(0); @include margin(0);
@include border-normal-left; @include border-normal-left;
background: var(--color-background-light);
.current-chat { .current-chat {
@include flex; @include flex;
@ -145,10 +135,9 @@
@include flex-weight(1); @include flex-weight(1);
@include margin($zero); @include margin($zero);
flex-direction: column; flex-direction: column;
// Firefox flexbox fix
height: 100%; height: 100%;
margin-bottom: $space-small;
overflow-y: auto; overflow-y: auto;
padding-bottom: var(--space-normal);
position: relative; position: relative;
} }
@ -171,7 +160,7 @@
@include elegant-card; @include elegant-card;
@include round-corner; @include round-corner;
background: $color-woot; background: $color-woot;
color: $color-white; color: var(--white);
font-size: $font-size-mini; font-size: $font-size-mini;
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
margin: $space-one auto; margin: $space-one auto;
@ -201,6 +190,10 @@
color: $color-body; color: $color-body;
margin-right: auto; margin-right: auto;
&.is-image {
border-radius: var(--border-radius-large);
}
.link { .link {
color: $color-primary-dark; color: $color-primary-dark;
} }
@ -218,6 +211,7 @@
color: $color-primary-dark; color: $color-primary-dark;
} }
} }
} }
+.right { +.right {
@ -247,7 +241,6 @@
background: lighten($warning-color, 32%); background: lighten($warning-color, 32%);
border: 1px solid lighten($warning-color, 15%); border: 1px solid lighten($warning-color, 15%);
color: $color-heading; color: $color-heading;
padding-right: $space-large;
position: relative; position: relative;
&::before { &::before {
@ -258,6 +251,10 @@
top: $space-smaller + $space-micro; top: $space-smaller + $space-micro;
} }
} }
&.is-image {
border-radius: var(--border-radius-large);
}
} }
+.left { +.left {
@ -297,30 +294,15 @@
border-radius: $space-smaller; border-radius: $space-smaller;
font-size: $font-size-small; font-size: $font-size-small;
p { .message-text__wrap {
color: $color-heading; display: inline-block;
margin-bottom: $zero;
.ion-person {
color: $color-body;
font-size: $font-size-default;
margin-right: $space-small;
position: relative;
top: $space-micro;
}
.message-text__wrap {
position: relative;
}
.message-text {
&::after {
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
display: inline;
}
}
} }
}
}
.activity-wrap .message-text__wrap {
.text-content p {
margin-bottom: 0;
} }
} }
@ -341,7 +323,7 @@
.typing-indicator { .typing-indicator {
@include elegant-card; @include elegant-card;
@include round-corner; @include round-corner;
background: $color-white; background: var(--white);
color: $color-light-gray; color: $color-light-gray;
font-size: $font-size-mini; font-size: $font-size-mini;
font-weight: $font-weight-bold; font-weight: $font-weight-bold;
@ -354,3 +336,65 @@
} }
} }
} }
.left .bubble .text-content {
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--color-body);
}
a {
color: var(--color-woot);
text-decoration: underline;
}
blockquote {
border-left-color: var(--s-300);
p {
color: var(--s-300);
}
}
p:last-child {
margin-bottom: 0;
}
}
.right .bubble .text-content {
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--white);
}
a {
color: var(--white);
text-decoration: underline;
}
blockquote {
border-left-color: var(--w-100);
p {
color: var(--w-100);
}
}
pre code {
background: var(--color-background);
}
p:last-child {
margin-bottom: 0;
}
}

View File

@ -2,12 +2,13 @@
#{$all-text-inputs}, #{$all-text-inputs},
select, select,
.multiselect > .multiselect__tags { .multiselect > .multiselect__tags {
@include thin-border(darken(get-color(alert), 25%)); @include thin-border(var(--r-400));
} }
.message { .message {
color: darken(get-color(alert), 25%); color: var(--r-400);
display: block; display: block;
font-size: var(--font-size-small);
font-weight: $font-weight-normal; font-weight: $font-weight-normal;
margin-bottom: $space-one; margin-bottom: $space-one;
margin-top: -$space-normal; margin-top: -$space-normal;

View File

@ -1,28 +1,13 @@
.reply-box { .reply-box {
@include light-shadow;
border-bottom: 0;
border-radius: $space-small;
margin: $space-normal;
margin-top: 0;
max-height: $space-mega * 3;
transition: box-shadow .35s $swift-ease-out-function, transition: box-shadow .35s $swift-ease-out-function,
height 2s $swift-ease-out-function; height 2s $swift-ease-out-function;
&.is-focused { &.is-focused {
@include shadow; box-shadow: var(--shadow);
} }
.reply-box__top { .reply-box__top {
@include flex;
@include flex-align($x: left, $y: middle);
@include padding($space-one $space-normal);
@include background-white;
@include margin(0);
border-top-left-radius: $space-small;
border-top-right-radius: $space-small;
position: relative;
.canned { .canned {
@include elegant-card; @include elegant-card;
background: $color-white; background: $color-white;
@ -41,19 +26,6 @@
} }
} }
&.is-active {
border-bottom-left-radius: $space-small;
border-bottom-right-radius: $space-small;
}
&.is-private {
background: lighten($warning-color, 38%);
>input {
background: lighten($warning-color, 38%);
}
}
.icon { .icon {
color: $medium-gray; color: $medium-gray;
cursor: pointer; cursor: pointer;
@ -65,9 +37,6 @@
} }
} }
.file-uploads>label {
cursor: pointer;
}
.attachment { .attachment {
cursor: pointer; cursor: pointer;
@ -82,87 +51,45 @@
// Override min-height : 50px in foundation // Override min-height : 50px in foundation
// //
max-height: $space-mega * 2.4; max-height: $space-mega * 2.4;
min-height: 4rem; min-height: 4.8rem;
padding: var(--space-normal) 0 0;
resize: none; resize: none;
} }
} }
.reply-box__bottom { &.is-private {
@include background-light; background: var(--y-50);
@include flex;
@include flex-align($x: justify, $y: middle);
@include border-light-top;
border-bottom-left-radius: $space-small;
border-bottom-right-radius: $space-small;
.tabs { .reply-box__top {
border: 0; background: var(--y-50);
flex: 1;
padding: 0;
.tabs-title { >input {
margin: 0; background: var(--y-50);
transition: all .2s $swift-ease-out-function;
transition-property: color, background;
a {
font-weight: $font-weight-medium;
padding: $space-one $space-two;
}
&.is-private.is-active {
background: lighten($warning-color, 38%);
a {
border-bottom-color: darken($warning-color, 15%);
color: darken($warning-color, 15%);
}
}
}
.tabs-title:first-child {
border-bottom-left-radius: $space-small;
&.is-active {
@include border-light-right;
border-left: 0;
}
a {
border-bottom-left-radius: $space-small;
}
}
.is-active {
@include background-white;
@include border-light-left;
@include border-light-right;
margin-top: -1px;
}
.message-length {
float: right;
a {
font-size: $font-size-mini;
}
}
.message-error {
color: $input-error-color;
}
}
.send-button {
border-bottom-right-radius: $space-small;
height: 3.6rem;
padding-left: $space-two;
padding-right: $space-two;
padding-top: $space-small;
.icon {
margin-left: $space-small;
} }
} }
} }
.file-uploads>label {
cursor: pointer;
&:hover .button--emoji {
background: var(--b-200);
}
}
.bottom-box .button--emoji.button--upload {
height: var(--space-large);
padding: 0;
width: var(--space-large);
.file-uploads {
height: 100%;
line-height: var(--space-large);
width: 100%;
}
label {
padding: var(--space-small);
}
}
} }

View File

@ -42,5 +42,9 @@ export default {
font-weight: $font-weight-medium; font-weight: $font-weight-medium;
margin-bottom: 0; margin-bottom: 0;
} }
.title--section {
padding-right: var(--space-large);
}
} }
</style> </style>

View File

@ -1,7 +1,7 @@
<template> <template>
<button <button
type="button" type="button"
class="button nice resolve--button" class="button resolve--button"
:class="buttonClass" :class="buttonClass"
@click="toggleStatus" @click="toggleStatus"
> >

View File

@ -0,0 +1,150 @@
<template>
<div>
<div
v-for="(attachment, index) in attachments"
:key="attachment.id"
class="preview-item"
>
<div class="thumb-wrap">
<img
v-if="isTypeImage(attachment.resource.type)"
class="image-thumb"
:src="attachment.thumb"
/>
<span v-else class="attachment-thumb">
📄
</span>
</div>
<div class="file-name-wrap">
<span class="item">
{{ attachment.resource.name }}
</span>
</div>
<div class="file-size-wrap">
<span class="item">
{{ formatFileSize(attachment.resource.size) }}
</span>
</div>
<div class="remove-file-wrap">
<button
class="remove--attachment"
@click="() => onRemoveAttachment(index)"
>
<i class="ion-android-close"></i>
</button>
</div>
</div>
</div>
</template>
<script>
import { formatBytes } from 'dashboard/helper/files';
export default {
props: {
attachments: {
type: Array,
default: () => [],
},
removeAttachment: {
type: Function,
default: () => {},
},
},
methods: {
onRemoveAttachment(index) {
this.removeAttachment(index);
},
formatFileSize(size) {
return formatBytes(size, 0);
},
isTypeImage(type) {
return type.includes('image');
},
},
};
</script>
<style lang="scss" scoped>
.preview-item {
display: flex;
padding: var(--space-slab) 0 0;
background: var(--color-background-light);
background: transparent;
}
.thumb-wrap {
max-width: var(--space-jumbo);
flex-shrink: 0;
width: var(--space-medium);
display: flex;
align-items: center;
}
.image-thumb {
width: var(--space-medium);
height: var(--space-medium);
object-fit: cover;
border-radius: var(--border-radius-small);
}
.attachment-thumb {
width: var(--space-medium);
height: var(--space-medium);
font-size: var(--font-size-medium);
text-align: center;
position: relative;
top: -1px;
text-align: left;
}
.file-name-wrap,
.file-size-wrap {
display: flex;
align-items: center;
padding: 0 var(--space-smaller);
> .item {
margin: 0;
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
}
}
.preview-header {
padding: var(--space-slab) var(--space-slab) 0 var(--space-slab);
}
.file-name-wrap {
max-width: 50%;
overflow: hidden;
text-overflow: ellipsis;
.item {
height: var(--space-normal);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.file-size-wrap {
width: 20%;
justify-content: center;
}
.remove-file-wrap {
display: flex;
align-items: center;
justify-content: center;
}
.remove--attachment {
width: var(--space-medium);
height: var(--space-medium);
border-radius: var(--space-medium);
font-size: var(--font-size-small);
cursor: pointer;
&:hover {
background: var(--color-background);
}
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<div ref="editor" class="editor-root"></div>
</template>
<script>
import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import {
schema,
defaultMarkdownParser,
defaultMarkdownSerializer,
} from 'prosemirror-markdown';
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
const TYPING_INDICATOR_IDLE_TIME = 4000;
const createState = (content, placeholder) =>
EditorState.create({
doc: defaultMarkdownParser.parse(content),
plugins: wootWriterSetup({ schema, placeholder }),
});
export default {
name: 'WootMessageEditor',
props: {
value: { type: String, default: '' },
placeholder: { type: String, default: '' },
},
data() {
return {
lastValue: null,
};
},
watch: {
value(newValue) {
if (newValue !== this.lastValue) {
this.state = createState(newValue, this.placeholder);
this.view.updateState(this.state);
}
},
},
created() {
this.state = createState(this.value, this.placeholder);
},
mounted() {
this.view = new EditorView(this.$refs.editor, {
state: this.state,
dispatchTransaction: tx => {
this.state = this.state.apply(tx);
this.view.updateState(this.state);
this.lastValue = defaultMarkdownSerializer.serialize(this.state.doc);
this.$emit('input', this.lastValue);
},
handleDOMEvents: {
keyup: () => {
this.onKeyup();
},
focus: () => {
this.onFocus();
},
blur: () => {
this.onBlur();
},
},
});
},
methods: {
resetTyping() {
this.$emit('typing-off');
this.idleTimer = null;
},
turnOffIdleTimer() {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
}
},
onKeyup() {
if (!this.idleTimer) {
this.$emit('typing-on');
}
this.turnOffIdleTimer();
this.idleTimer = setTimeout(
() => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME
);
},
onBlur() {
this.turnOffIdleTimer();
this.resetTyping();
this.$emit('blur');
},
onFocus() {
this.$emit('focus');
},
},
};
</script>
<style lang="scss">
.ProseMirror-menubar-wrapper {
display: flex;
flex-direction: column;
> .ProseMirror {
padding: 0;
}
}
.editor-root {
width: 100%;
}
.ProseMirror-woot-style {
min-height: 8rem;
max-height: 12rem;
overflow: auto;
}
</style>

View File

@ -0,0 +1,188 @@
<template>
<div class="bottom-box" :class="wrapClass">
<div class="left-wrap">
<button
class="button clear button--emoji"
:title="$t('CONVERSATION.REPLYBOX.TIP_EMOJI_ICON')"
@click="toggleEmojiPicker"
>
<emoji-or-icon icon="ion-happy-outline" emoji="😊" />
</button>
<button
v-if="showAttachButton"
class="button clear button--emoji button--upload"
:title="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
>
<file-upload
:size="4096 * 4096"
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
@input-file="onFileUpload"
>
<emoji-or-icon icon="ion-android-attach" emoji="📎" />
</file-upload>
</button>
<button
v-if="enableRichEditor"
class="button clear button--emoji"
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
@click="toggleFormatMode"
>
<emoji-or-icon icon="ion-quote" emoji="🖊️" />
</button>
</div>
<div class="right-wrap">
<button
class="button nice primary button--send"
:class="buttonClass"
@click="onSend"
>
{{ sendButtonText }}
</button>
</div>
</div>
</template>
<script>
import FileUpload from 'vue-upload-component';
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
import { REPLY_EDITOR_MODES } from './constants';
export default {
name: 'ReplyTopPanel',
components: { EmojiOrIcon, FileUpload },
props: {
mode: {
type: String,
default: REPLY_EDITOR_MODES.REPLY,
},
onSend: {
type: Function,
default: () => {},
},
sendButtonText: {
type: String,
default: '',
},
showFileUpload: {
type: Boolean,
default: false,
},
onFileUpload: {
type: Function,
default: () => {},
},
showEmojiPicker: {
type: Boolean,
default: false,
},
toggleEmojiPicker: {
type: Function,
default: () => {},
},
isSendDisabled: {
type: Boolean,
default: false,
},
setFormatMode: {
type: Function,
default: () => {},
},
isFormatMode: {
type: Boolean,
default: false,
},
enableRichEditor: {
type: Boolean,
default: false,
},
},
computed: {
isNote() {
return this.mode === REPLY_EDITOR_MODES.NOTE;
},
wrapClass() {
return {
'is-note-mode': this.isNote,
};
},
buttonClass() {
return {
'button--note': this.isNote,
'button--disabled': this.isSendDisabled,
};
},
showAttachButton() {
return this.showFileUpload || this.isNote;
},
},
methods: {
toggleFormatMode() {
this.setFormatMode(!this.isFormatMode);
},
},
};
</script>
<style lang="scss" scoped>
@import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';
.bottom-box {
display: flex;
justify-content: space-between;
padding: var(--space-slab) var(--space-normal);
&.is-note-mode {
background: var(--y-50);
}
}
.button {
display: flex;
align-items: center;
justify-content: space-between;
&.is-active {
background: white;
}
&.button--note {
background: var(--y-800);
color: white;
&:hover {
background: var(--y-700);
}
}
&.button--disabled {
background: var(--b-100);
color: var(--b-400);
cursor: default;
&:hover {
background: var(--b-100);
}
}
}
.bottom-box.is-note-mode {
.button--emoji {
background: white;
}
}
.left-wrap {
display: flex;
align-items: center;
}
.button--reply {
border-right: 1px solid var(--color-border-light);
}
.icon--font {
color: var(--s-600);
font-size: var(--font-size-default);
}
</style>

View File

@ -0,0 +1,156 @@
<template>
<div class="top-box">
<div class="mode-wrap button-group">
<button
class="button clear button--reply"
:class="replyButtonClass"
@click="handleReplyClick"
>
<emoji-or-icon icon="" emoji="💬" />
{{ $t('CONVERSATION.REPLYBOX.REPLY') }}
</button>
<button
class="button clear button--note"
:class="noteButtonClass"
@click="handleNoteClick"
>
<emoji-or-icon icon="" emoji="📝" />
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
</button>
</div>
<div class="action-wrap">
<div v-if="isMessageLengthReachingThreshold" class="tabs-title">
<span :class="charLengthClass">
{{ characterLengthWarning }}
</span>
</div>
</div>
</div>
</template>
<script>
import { REPLY_EDITOR_MODES, CHAR_LENGTH_WARNING } from './constants';
import EmojiOrIcon from 'shared/components/EmojiOrIcon';
export default {
name: 'ReplyTopPanel',
components: {
EmojiOrIcon,
},
props: {
mode: {
type: String,
default: REPLY_EDITOR_MODES.REPLY,
},
setReplyMode: {
type: Function,
default: () => {},
},
isMessageLengthReachingThreshold: {
type: Boolean,
default: () => false,
},
charactersRemaining: {
type: Number,
default: () => 0,
},
},
computed: {
replyButtonClass() {
return {
'is-active': this.mode === REPLY_EDITOR_MODES.REPLY,
};
},
noteButtonClass() {
return {
'is-active': this.mode === REPLY_EDITOR_MODES.NOTE,
};
},
charLengthClass() {
return this.charactersRemaining < 0 ? 'message-error' : 'message-length';
},
characterLengthWarning() {
return this.charactersRemaining < 0
? `${-this.charactersRemaining} ${CHAR_LENGTH_WARNING.NEGATIVE}`
: `${this.charactersRemaining} ${CHAR_LENGTH_WARNING.UNDER_50}`;
},
},
methods: {
handleReplyClick() {
this.setReplyMode(REPLY_EDITOR_MODES.REPLY);
},
handleNoteClick() {
this.setReplyMode(REPLY_EDITOR_MODES.NOTE);
},
},
};
</script>
<style lang="scss" scoped>
.top-box {
display: flex;
justify-content: space-between;
background: var(--b-100);
}
.button-group {
border: 0;
padding: 0;
margin: 0;
.button {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
padding: var(--space-one) var(--space-normal);
margin: 0;
position: relative;
z-index: 1;
&.is-active {
background: white;
}
}
.button--reply {
border-radius: 0;
border-right: 1px solid var(--color-border);
&:hover {
border-right: 1px solid var(--color-border);
}
}
.button--note {
border-radius: 0;
&.is-active {
border-right: 1px solid var(--color-border);
background: var(--y-50);
}
&:hover,
&:active {
color: var(--y-800);
}
}
}
.button--note {
color: var(--y-900);
}
.action-wrap {
display: flex;
align-items: center;
margin: 0 var(--space-normal);
font-size: var(--font-size-mini);
.message-error {
color: var(--r-600);
}
.message-length {
color: var(--s-600);
}
}
</style>

View File

@ -0,0 +1,9 @@
export const REPLY_EDITOR_MODES = {
REPLY: 'REPLY',
NOTE: 'NOTE',
};
export const CHAR_LENGTH_WARNING = {
UNDER_50: 'characters remaining',
NEGATIVE: 'characters over',
};

View File

@ -27,7 +27,7 @@
<p v-if="lastMessageInChat" class="conversation--message"> <p v-if="lastMessageInChat" class="conversation--message">
<i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i> <i v-if="messageByAgent" class="ion-ios-undo message-from-agent"></i>
<span v-if="lastMessageInChat.content"> <span v-if="lastMessageInChat.content">
{{ lastMessageInChat.content }} {{ parsedLastMessage }}
</span> </span>
<span v-else-if="!lastMessageInChat.attachments">{{ ` ` }}</span> <span v-else-if="!lastMessageInChat.attachments">{{ ` ` }}</span>
<span v-else> <span v-else>
@ -47,6 +47,7 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { MESSAGE_TYPE } from 'widget/helpers/constants'; import { MESSAGE_TYPE } from 'widget/helpers/constants';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
import conversationMixin from '../../../mixins/conversations'; import conversationMixin from '../../../mixins/conversations';
@ -59,7 +60,7 @@ export default {
Thumbnail, Thumbnail,
}, },
mixins: [timeMixin, conversationMixin], mixins: [timeMixin, conversationMixin, messageFormatterMixin],
props: { props: {
activeLabel: { activeLabel: {
type: String, type: String,
@ -129,6 +130,10 @@ export default {
const { message_type: messageType } = lastMessage; const { message_type: messageType } = lastMessage;
return messageType === MESSAGE_TYPE.OUTGOING; return messageType === MESSAGE_TYPE.OUTGOING;
}, },
parsedLastMessage() {
return this.getPlainText(this.lastMessageInChat.content);
},
}, },
methods: { methods: {

View File

@ -1,15 +1,21 @@
<template> <template>
<li v-if="hasAttachments || data.content" :class="alignBubble"> <li v-if="hasAttachments || data.content" :class="alignBubble">
<div :class="wrapClass"> <div :class="wrapClass">
<p v-tooltip.top-start="sentByMessage" :class="bubbleClass"> <div v-tooltip.top-start="sentByMessage" :class="bubbleClass">
<bubble-text <bubble-text
v-if="data.content" v-if="data.content"
:message="message" :message="message"
:is-email="isEmailContentType" :is-email="isEmailContentType"
:readable-time="readableTime" :readable-time="readableTime"
/> />
<span v-if="hasAttachments"> <span
<span v-for="attachment in data.attachments" :key="attachment.id"> v-if="isPending && hasAttachments"
class="chat-bubble has-attachment agent"
>
{{ $t('CONVERSATION.UPLOADING_ATTACHMENTS') }}
</span>
<div v-if="!isPending && hasAttachments">
<div v-for="attachment in data.attachments" :key="attachment.id">
<bubble-image <bubble-image
v-if="attachment.file_type === 'image'" v-if="attachment.file_type === 'image'"
:url="attachment.data_url" :url="attachment.data_url"
@ -20,8 +26,9 @@
:url="attachment.data_url" :url="attachment.data_url"
:readable-time="readableTime" :readable-time="readableTime"
/> />
</span> </div>
</span> </div>
<bubble-actions <bubble-actions
:id="data.id" :id="data.id"
:sender="data.sender" :sender="data.sender"
@ -32,9 +39,16 @@
:readable-time="readableTime" :readable-time="readableTime"
:source-id="data.source_id" :source-id="data.source_id"
/> />
</p> </div>
<spinner v-if="isPending" size="tiny" />
<div v-if="isATweet && isIncoming && sender" class="sender--info"> <a
v-if="isATweet && isIncoming && sender"
class="sender--info"
:href="twitterProfileLink"
target="_blank"
rel="noopener noreferrer nofollow"
>
<woot-thumbnail <woot-thumbnail
:src="sender.thumbnail" :src="sender.thumbnail"
:username="sender.name" :username="sender.name"
@ -43,7 +57,7 @@
<div class="sender--available-name"> <div class="sender--available-name">
{{ sender.name }} {{ sender.name }}
</div> </div>
</div> </a>
</div> </div>
</li> </li>
</template> </template>
@ -53,9 +67,11 @@ import timeMixin from '../../../mixins/time';
import BubbleText from './bubble/Text'; import BubbleText from './bubble/Text';
import BubbleImage from './bubble/Image'; import BubbleImage from './bubble/Image';
import BubbleFile from './bubble/File'; import BubbleFile from './bubble/File';
import Spinner from 'shared/components/Spinner';
import contentTypeMixin from 'shared/mixins/contentTypeMixin'; import contentTypeMixin from 'shared/mixins/contentTypeMixin';
import BubbleActions from './bubble/Actions'; import BubbleActions from './bubble/Actions';
import { MESSAGE_TYPE } from 'shared/constants/messageTypes'; import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
export default { export default {
components: { components: {
@ -63,6 +79,7 @@ export default {
BubbleText, BubbleText,
BubbleImage, BubbleImage,
BubbleFile, BubbleFile,
Spinner,
}, },
mixins: [timeMixin, messageFormatterMixin, contentTypeMixin], mixins: [timeMixin, messageFormatterMixin, contentTypeMixin],
props: { props: {
@ -93,6 +110,11 @@ export default {
} = this; } = this;
return contentType; return contentType;
}, },
twitterProfileLink() {
const additionalAttributes = this.sender.additional_attributes || {};
const { screen_name: screenName } = additionalAttributes;
return `https://twitter.com/${screenName}`;
},
alignBubble() { alignBubble() {
return !this.data.message_type ? 'left' : 'right'; return !this.data.message_type ? 'left' : 'right';
}, },
@ -116,6 +138,9 @@ export default {
} }
return false; return false;
}, },
hasText() {
return !!this.data.content;
},
sentByMessage() { sentByMessage() {
const { sender } = this; const { sender } = this;
@ -130,6 +155,7 @@ export default {
return { return {
wrap: this.isBubble, wrap: this.isBubble,
'activity-wrap': !this.isBubble, 'activity-wrap': !this.isBubble,
'is-pending': this.isPending,
}; };
}, },
bubbleClass() { bubbleClass() {
@ -137,25 +163,58 @@ export default {
bubble: this.isBubble, bubble: this.isBubble,
'is-private': this.data.private, 'is-private': this.data.private,
'is-image': this.hasImageAttachment, 'is-image': this.hasImageAttachment,
'is-text': this.hasText,
}; };
}, },
isPending() {
return this.data.status === MESSAGE_STATUS.PROGRESS;
},
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss">
.wrap > .is-image.bubble { .wrap {
padding: 0; > .bubble {
overflow: hidden; &.is-image {
padding: 0;
overflow: hidden;
.image { .image {
max-width: 32rem; max-width: 32rem;
padding: 0; padding: var(--space-micro);
> img {
border-radius: var(--border-radius-medium);
}
}
}
&.is-image.is-text > .message-text__wrap {
max-width: 32rem;
padding: var(--space-small) var(--space-normal);
}
}
&.is-pending {
position: relative;
opacity: 0.8;
.spinner {
position: absolute;
bottom: var(--space-smaller);
right: var(--space-smaller);
}
> .is-image.is-text.bubble > .message-text__wrap {
padding: 0;
}
} }
} }
.sender--info { .sender--info {
display: flex;
align-items: center; align-items: center;
color: var(--b-700);
display: inline-flex;
padding: var(--space-smaller) 0; padding: var(--space-smaller) 0;
.sender--available-name { .sender--available-name {

View File

@ -1,6 +1,12 @@
<template> <template>
<div class="reply-box" :class="replyBoxClass"> <div class="reply-box" :class="replyBoxClass">
<div class="reply-box__top" :class="{ 'is-private': isPrivate }"> <reply-top-panel
:mode="replyType"
:set-reply-mode="setReplyMode"
:is-message-length-reaching-threshold="isMessageLengthReachingThreshold"
:characters-remaining="charactersRemaining"
/>
<div class="reply-box__top">
<canned-response <canned-response
v-if="showCannedResponsesList" v-if="showCannedResponsesList"
v-on-clickaway="hideCannedResponse" v-on-clickaway="hideCannedResponse"
@ -14,6 +20,7 @@
:on-click="emojiOnClick" :on-click="emojiOnClick"
/> />
<resizable-text-area <resizable-text-area
v-if="!isFormatMode"
ref="messageInput" ref="messageInput"
v-model="message" v-model="message"
class="input" class="input"
@ -24,71 +31,52 @@
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
/> />
<file-upload <woot-message-editor
v-if="showFileUpload" v-else
:size="4096 * 4096" v-model="message"
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv" class="input"
@input-file="onFileUpload" :placeholder="messagePlaceHolder"
> :min-height="4"
<i v-if="!isUploading" class="icon ion-android-attach attachment" /> @typing-off="onTypingOff"
<woot-spinner v-if="isUploading" /> @typing-on="onTypingOn"
</file-upload> @focus="onFocus"
<i @blur="onBlur"
class="icon ion-happy-outline"
:class="{ active: showEmojiPicker }"
@click="toggleEmojiPicker"
/> />
</div> </div>
<div v-if="hasAttachments" class="attachment-preview-box">
<div class="reply-box__bottom"> <attachment-preview
<ul class="tabs"> :attachments="attachedFiles"
<li class="tabs-title" :class="{ 'is-active': !isPrivate }"> :remove-attachment="removeAttachment"
<a href="#" @click="setReplyMode">{{ />
$t('CONVERSATION.REPLYBOX.REPLY')
}}</a>
</li>
<li class="tabs-title is-private" :class="{ 'is-active': isPrivate }">
<a href="#" @click="setPrivateReplyMode">
{{ $t('CONVERSATION.REPLYBOX.PRIVATE_NOTE') }}
</a>
</li>
<li v-if="message.length" class="tabs-title message-length">
<a :class="{ 'message-error': isMessageLengthReachingThreshold }">
{{ characterCountIndicator }}
</a>
</li>
</ul>
<button
type="button"
class="button send-button"
:disabled="isReplyButtonDisabled"
:class="{
disabled: isReplyButtonDisabled,
warning: isPrivate,
}"
@click="sendMessage"
>
{{ replyButtonLabel }}
<i
class="icon"
:class="{
'ion-android-send': !isPrivate,
'ion-android-lock': isPrivate,
}"
/>
</button>
</div> </div>
<reply-bottom-panel
:mode="replyType"
:send-button-text="replyButtonLabel"
:on-file-upload="onFileUpload"
:show-file-upload="showFileUpload"
:toggle-emoji-picker="toggleEmojiPicker"
:show-emoji-picker="showEmojiPicker"
:on-send="sendMessage"
:is-send-disabled="isReplyButtonDisabled"
:set-format-mode="setFormatMode"
:is-format-mode="isFormatMode"
:enable-rich-editor="isRichEditorEnabled"
/>
</div> </div>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import FileUpload from 'vue-upload-component';
import EmojiInput from 'shared/components/emoji/EmojiInput'; import EmojiInput from 'shared/components/emoji/EmojiInput';
import CannedResponse from './CannedResponse'; import CannedResponse from './CannedResponse';
import ResizableTextArea from 'shared/components/ResizableTextArea'; import ResizableTextArea from 'shared/components/ResizableTextArea';
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel';
import ReplyBottomPanel from 'dashboard/components/widgets/WootWriter/ReplyBottomPanel';
import { REPLY_EDITOR_MODES } from 'dashboard/components/widgets/WootWriter/constants';
import WootMessageEditor from 'dashboard/components/widgets/WootWriter/Editor';
import { import {
isEscape, isEscape,
isEnter, isEnter,
@ -101,8 +89,11 @@ export default {
components: { components: {
EmojiInput, EmojiInput,
CannedResponse, CannedResponse,
FileUpload,
ResizableTextArea, ResizableTextArea,
AttachmentPreview,
ReplyTopPanel,
ReplyBottomPanel,
WootMessageEditor,
}, },
mixins: [clickaway, inboxMixin], mixins: [clickaway, inboxMixin],
props: { props: {
@ -114,18 +105,20 @@ export default {
data() { data() {
return { return {
message: '', message: '',
isPrivateTabActive: false,
isFocused: false, isFocused: false,
showEmojiPicker: false, showEmojiPicker: false,
showCannedResponsesList: false, showCannedResponsesList: false,
attachedFiles: [],
isUploading: false, isUploading: false,
replyType: REPLY_EDITOR_MODES.REPLY,
isFormatMode: false,
}; };
}, },
computed: { computed: {
...mapGetters({ currentChat: 'getSelectedChat' }), ...mapGetters({ currentChat: 'getSelectedChat' }),
isPrivate() { isPrivate() {
if (this.currentChat.can_reply) { if (this.currentChat.can_reply) {
return this.isPrivateTabActive; return this.replyType === REPLY_EDITOR_MODES.NOTE;
} }
return true; return true;
}, },
@ -141,13 +134,15 @@ export default {
: this.$t('CONVERSATION.FOOTER.MSG_INPUT'); : this.$t('CONVERSATION.FOOTER.MSG_INPUT');
}, },
isMessageLengthReachingThreshold() { isMessageLengthReachingThreshold() {
return this.message.length > this.maxLength - 40; return this.message.length > this.maxLength - 50;
}, },
characterCountIndicator() { charactersRemaining() {
return `${this.message.length} / ${this.maxLength}`; return this.maxLength - this.message.length;
}, },
isReplyButtonDisabled() { isReplyButtonDisabled() {
const isMessageEmpty = !this.message.trim().replace(/\n/g, '').length; const isMessageEmpty = !this.message.trim().replace(/\n/g, '').length;
if (this.hasAttachments) return false;
return ( return (
isMessageEmpty || isMessageEmpty ||
this.message.length === 0 || this.message.length === 0 ||
@ -196,16 +191,28 @@ export default {
}, },
replyBoxClass() { replyBoxClass() {
return { return {
'is-focused': this.isFocused, 'is-private': this.isPrivate,
'is-focused': this.isFocused || this.hasAttachments,
}; };
}, },
hasAttachments() {
return this.attachedFiles.length;
},
isRichEditorEnabled() {
return (
this.isAWebWidgetInbox ||
this.isAnEmailChannel ||
this.replyType === REPLY_EDITOR_MODES.NOTE
);
},
}, },
watch: { watch: {
currentChat(conversation) { currentChat(conversation) {
if (conversation.can_reply) { const { can_reply: canReply } = conversation;
this.isPrivateTabActive = false; if (canReply) {
this.replyType = REPLY_EDITOR_MODES.REPLY;
} else { } else {
this.isPrivateTabActive = true; this.replyType = REPLY_EDITOR_MODES.NOTE;
} }
}, },
message(updatedMessage) { message(updatedMessage) {
@ -240,7 +247,9 @@ export default {
this.hideEmojiPicker(); this.hideEmojiPicker();
this.hideCannedResponse(); this.hideCannedResponse();
} else if (isEnter(e)) { } else if (isEnter(e)) {
if (!hasPressedShift(e)) { const shouldSendMessage =
!this.isFormatMode && !hasPressedShift(e) && this.isFocused;
if (shouldSendMessage) {
e.preventDefault(); e.preventDefault();
this.sendMessage(); this.sendMessage();
} }
@ -250,18 +259,11 @@ export default {
if (this.isReplyButtonDisabled) { if (this.isReplyButtonDisabled) {
return; return;
} }
const newMessage = this.message;
if (!this.showCannedResponsesList) { if (!this.showCannedResponsesList) {
const newMessage = this.message;
const messagePayload = this.getMessagePayload(newMessage);
this.clearMessage(); this.clearMessage();
try { try {
const messagePayload = {
conversationId: this.currentChat.id,
message: newMessage,
private: this.isPrivate,
};
if (this.inReplyTo) {
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
}
await this.$store.dispatch('sendMessage', messagePayload); await this.$store.dispatch('sendMessage', messagePayload);
this.$emit('scrollToMessage'); this.$emit('scrollToMessage');
} catch (error) { } catch (error) {
@ -275,12 +277,10 @@ export default {
this.message = message; this.message = message;
}, 100); }, 100);
}, },
setPrivateReplyMode() { setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
this.isPrivateTabActive = true; const { can_reply: canReply } = this.currentChat;
this.$refs.messageInput.focus();
}, if (canReply) this.replyType = mode;
setReplyMode() {
this.isPrivateTabActive = false;
this.$refs.messageInput.focus(); this.$refs.messageInput.focus();
}, },
emojiOnClick(emoji) { emojiOnClick(emoji) {
@ -288,6 +288,7 @@ export default {
}, },
clearMessage() { clearMessage() {
this.message = ''; this.message = '';
this.attachedFiles = [];
}, },
toggleEmojiPicker() { toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker; this.showEmojiPicker = !this.showEmojiPicker;
@ -322,30 +323,91 @@ export default {
} }
}, },
onFileUpload(file) { onFileUpload(file) {
this.attachedFiles = [];
if (!file) { if (!file) {
return; return;
} }
this.isUploading = true; const reader = new FileReader();
this.$store reader.readAsDataURL(file.file);
.dispatch('sendAttachment', [
this.currentChat.id, reader.onloadend = () => {
{ file: file.file, isPrivate: this.isPrivate }, this.attachedFiles.push({
]) currentChatId: this.currentChat.id,
.then(() => { resource: file,
this.isUploading = false; isPrivate: this.isPrivate,
this.$emit('scrollToMessage'); thumb: reader.result,
})
.catch(() => {
this.isUploading = false;
this.$emit('scrollToMessage');
}); });
};
},
removeAttachment(itemIndex) {
this.attachedFiles = this.attachedFiles.filter(
(item, index) => itemIndex !== index
);
},
getMessagePayload(message) {
const [attachment] = this.attachedFiles;
const messagePayload = {
conversationId: this.currentChat.id,
message,
private: this.isPrivate,
};
if (this.inReplyTo) {
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
}
if (attachment) {
messagePayload.file = attachment.resource.file;
}
return messagePayload;
},
setFormatMode(value) {
this.isFormatMode = value;
}, },
}, },
}; };
</script> </script>
<style lang="scss"> <style lang="scss" scoped>
.send-button { .send-button {
margin-bottom: 0; margin-bottom: 0;
} }
.attachment-preview-box {
padding: 0 var(--space-normal);
background: transparent;
}
.reply-box {
border-top: 1px solid var(--color-border);
background: white;
&.is-private {
background: var(--y-50);
}
}
.send-button {
margin-bottom: 0;
}
.reply-box__top {
padding: 0 var(--space-normal);
border-top: 1px solid var(--color-border);
margin-top: -1px;
}
.emoji-dialog {
top: unset;
bottom: 12px;
left: -320px;
right: unset;
&::before {
right: -16px;
bottom: 10px;
transform: rotate(270deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
}
}
</style> </style>

View File

@ -19,9 +19,13 @@
class="icon ion-reply cursor-pointer" class="icon ion-reply cursor-pointer"
@click="onTweetReply" @click="onTweetReply"
/> />
<a :href="linkToTweet" target="_blank" rel="noopener noreferrer nofollow"> <a
v-if="isATweet && isIncoming"
:href="linkToTweet"
target="_blank"
rel="noopener noreferrer nofollow"
>
<i <i
v-if="isATweet && isIncoming"
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')" v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
class="icon ion-android-open cursor-pointer" class="icon ion-android-open cursor-pointer"
/> />
@ -30,7 +34,7 @@
</template> </template>
<script> <script>
import { MESSAGE_TYPE } from 'shared/constants/messageTypes'; import { MESSAGE_TYPE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
export default { export default {

View File

@ -1,7 +1,7 @@
<template> <template>
<span class="message-text__wrap"> <div class="message-text__wrap">
<span v-html="message"></span> <div class="text-content" v-html="message"></div>
</span> </div>
</template> </template>
<script> <script>

View File

@ -6,8 +6,12 @@
:type="type" :type="type"
:placeholder="placeholder" :placeholder="placeholder"
@input="onChange" @input="onChange"
@blur="onBlur"
/> />
<p v-if="helpText" class="help-text"></p> <p v-if="helpText" class="help-text"></p>
<span v-if="error" class="message">
{{ error }}
</span>
</label> </label>
</template> </template>
@ -34,11 +38,18 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
error: {
type: String,
default: '',
},
}, },
methods: { methods: {
onChange(e) { onChange(e) {
this.$emit('input', e.target.value); this.$emit('input', e.target.value);
}, },
onBlur(e) {
this.$emit('blur', e.target.value);
},
}, },
}; };
</script> </script>

View File

@ -1,5 +1,8 @@
/* eslint no-console: 0 */
/* eslint no-param-reassign: 0 */ /* eslint no-param-reassign: 0 */
import getUuid from 'widget/helpers/uuid';
import { MESSAGE_STATUS, MESSAGE_TYPE } from 'shared/constants/messages';
export default () => { export default () => {
if (!Array.prototype.last) { if (!Array.prototype.last) {
Object.assign(Array.prototype, { Object.assign(Array.prototype, {
@ -26,3 +29,23 @@ export const getTypingUsersText = (users = []) => {
const rest = users.length - 1; const rest = users.length - 1;
return `${user.name} and ${rest} others are typing`; return `${user.name} and ${rest} others are typing`;
}; };
export const createPendingMessage = data => {
const timestamp = Math.floor(new Date().getTime() / 1000);
const tempMessageId = getUuid();
const { message, file } = data;
const tempAttachments = [{ id: tempMessageId }];
const pendingMessage = {
...data,
content: message || null,
id: tempMessageId,
echo_id: tempMessageId,
status: MESSAGE_STATUS.PROGRESS,
created_at: timestamp,
message_type: MESSAGE_TYPE.OUTGOING,
conversation_id: data.conversationId,
attachments: file ? tempAttachments : null,
};
return pendingMessage;
};

View File

@ -0,0 +1,11 @@
export const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};

View File

@ -1,4 +1,4 @@
import { getTypingUsersText } from '../commons'; import { getTypingUsersText, createPendingMessage } from '../commons';
describe('#getTypingUsersText', () => { describe('#getTypingUsersText', () => {
it('returns the correct text is there is only one typing user', () => { it('returns the correct text is there is only one typing user', () => {
@ -24,3 +24,62 @@ describe('#getTypingUsersText', () => {
).toEqual('Pranav and 3 others are typing'); ).toEqual('Pranav and 3 others are typing');
}); });
}); });
describe('#createPendingMessage', () => {
const message = {
message: 'hi',
};
it('returns the pending message with expected new keys', () => {
expect(createPendingMessage(message)).toHaveProperty(
'content',
'id',
'status',
'echo_id',
'status',
'created_at',
'message_type',
'conversation_id'
);
});
it('returns the pending message with status progress', () => {
expect(createPendingMessage(message)).toMatchObject({
status: 'progress',
});
});
it('returns the pending message with same id and echo_id', () => {
const pending = createPendingMessage(message);
expect(pending).toMatchObject({
echo_id: pending.id,
});
});
it('returns the pending message with attachmnet key if file is passed', () => {
const messageWithFile = {
message: 'hi',
file: {},
};
expect(createPendingMessage(messageWithFile)).toHaveProperty(
'content',
'id',
'status',
'echo_id',
'status',
'created_at',
'message_type',
'conversation_id',
'attachments',
'private'
);
});
it('returns the pending message to have one attachment', () => {
const messageWithFile = {
message: 'hi',
file: {},
};
const pending = createPendingMessage(messageWithFile);
expect(pending.attachments.length).toBe(1);
});
});

View File

@ -0,0 +1,18 @@
import { formatBytes } from '../files';
describe('#File Helpers', () => {
describe('formatBytes', () => {
it('should return zero bytes if 0 is passed', () => {
expect(formatBytes(0)).toBe('0 Bytes');
});
it('should return in bytes if 1000 is passed', () => {
expect(formatBytes(1000)).toBe('1000 Bytes');
});
it('should return in KB if 100000 is passed', () => {
expect(formatBytes(10000)).toBe('9.77 KB');
});
it('should return in MB if 10000000 is passed', () => {
expect(formatBytes(10000000)).toBe('9.54 MB');
});
});
});

View File

@ -25,7 +25,7 @@ export const getSidebarItems = accountId => ({
toStateName: 'home', toStateName: 'home',
}, },
contacts: { contacts: {
icon: 'ion-person-stalker', icon: 'ion-person',
label: 'CONTACTS', label: 'CONTACTS',
hasSubMenu: false, hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/contacts`), toState: frontendURL(`accounts/${accountId}/contacts`),

View File

@ -11,6 +11,7 @@
"OS": "نظام التشغيل", "OS": "نظام التشغيل",
"INITIATED_FROM": "تم البدء من", "INITIATED_FROM": "تم البدء من",
"INITIATED_AT": "تم البدء في", "INITIATED_AT": "تم البدء في",
"IP_ADDRESS": "IP Address",
"CONVERSATIONS": { "CONVERSATIONS": {
"NO_RECORDS_FOUND": "لا توجد محادثات سابقة مرتبطة بجهة الاتصال هذه.", "NO_RECORDS_FOUND": "لا توجد محادثات سابقة مرتبطة بجهة الاتصال هذه.",
"TITLE": "المحادثات السابقة" "TITLE": "المحادثات السابقة"

View File

@ -24,6 +24,7 @@
"REPLYING_TO": "أنت ترد على:", "REPLYING_TO": "أنت ترد على:",
"REMOVE_SELECTION": "إزالة التحديد", "REMOVE_SELECTION": "إزالة التحديد",
"DOWNLOAD": "تنزيل", "DOWNLOAD": "تنزيل",
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "إغلاق المحادثة", "RESOLVE_ACTION": "إغلاق المحادثة",
"REOPEN_ACTION": "إعادة فتح", "REOPEN_ACTION": "إعادة فتح",
@ -40,7 +41,10 @@
"PRIVATE_NOTE": "إضافة ملاحظة خاصة", "PRIVATE_NOTE": "إضافة ملاحظة خاصة",
"SEND": "إرسال", "SEND": "إرسال",
"CREATE": "إضافة ملاحظة", "CREATE": "إضافة ملاحظة",
"TWEET": "تغريد" "TWEET": "تغريد",
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files"
}, },
"VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لأعضاء فريق العمل والموظفين", "VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لأعضاء فريق العمل والموظفين",
"CHANGE_STATUS": "تم تغيير حالة المحادثة", "CHANGE_STATUS": "تم تغيير حالة المحادثة",

View File

@ -42,7 +42,8 @@
"INBOUND_EMAIL_ENABLED": "الاستمرار في المحادثة عبر رسائل البريد الإلكتروني مفعّل لحسابك.", "INBOUND_EMAIL_ENABLED": "الاستمرار في المحادثة عبر رسائل البريد الإلكتروني مفعّل لحسابك.",
"CUSTOM_EMAIL_DOMAIN_ENABLED": "يمكنك تلقي رسائل البريد الإلكتروني في النطاق المخصص الخاص بك الآن." "CUSTOM_EMAIL_DOMAIN_ENABLED": "يمكنك تلقي رسائل البريد الإلكتروني في النطاق المخصص الخاص بك الآن."
} }
} },
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
}, },
"FORMS": { "FORMS": {
"MULTISELECT": { "MULTISELECT": {

View File

@ -241,7 +241,9 @@
"AUTO_ASSIGNMENT": "تفعيل الإسناد التلقائي", "AUTO_ASSIGNMENT": "تفعيل الإسناد التلقائي",
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل", "INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل", "INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه." "AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",
"HMAC_VERIFICATION": "User Identity Validation",
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
}, },
"FACEBOOK_REAUTHORIZE": { "FACEBOOK_REAUTHORIZE": {
"TITLE": "إعادة التصريح", "TITLE": "إعادة التصريح",

View File

@ -113,7 +113,7 @@
"SIDEBAR": { "SIDEBAR": {
"CONVERSATIONS": "المحادثات", "CONVERSATIONS": "المحادثات",
"REPORTS": "التقارير", "REPORTS": "التقارير",
"CONTACTS": "Contacts (Beta)", "CONTACTS": "Contacts",
"SETTINGS": "الإعدادات", "SETTINGS": "الإعدادات",
"HOME": "الرئيسية", "HOME": "الرئيسية",
"AGENTS": "موظف الدعم", "AGENTS": "موظف الدعم",

View File

@ -5,13 +5,18 @@
"TERMS_ACCEPT": "من خلال التسجيل، فإنك توافق على <a href=\"https://www.chatwoot.com/terms\">شروط الخدمة</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">سياسة الخصوصية</a>", "TERMS_ACCEPT": "من خلال التسجيل، فإنك توافق على <a href=\"https://www.chatwoot.com/terms\">شروط الخدمة</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">سياسة الخصوصية</a>",
"ACCOUNT_NAME": { "ACCOUNT_NAME": {
"LABEL": "اسم الحساب", "LABEL": "اسم الحساب",
"PLACEHOLDER": "مؤسسة Wayne", "PLACEHOLDER": "Enter an account name. eg: Wayne Enterprises",
"ERROR": "اسم الحساب قصير جداً" "ERROR": "Account name is too short"
},
"FULL_NAME": {
"LABEL": "Full name",
"PLACEHOLDER": "Enter your full name. eg: Bruce Wayne",
"ERROR": "Full name is too short"
}, },
"EMAIL": { "EMAIL": {
"LABEL": "البريد الإلكتروني", "LABEL": "Work email",
"PLACEHOLDER": "bruce@wayne.enterprises", "PLACEHOLDER": "Enter your work email address. eg: bruce@wayne.enterprises",
"ERROR": "البريد الإلكتروني غير صالح" "ERROR": "Email address is invalid"
}, },
"PASSWORD": { "PASSWORD": {
"LABEL": "كلمة المرور", "LABEL": "كلمة المرور",
@ -28,12 +33,6 @@
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً" "ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
}, },
"SUBMIT": "إرسال", "SUBMIT": "إرسال",
"FEATURES": { "HAVE_AN_ACCOUNT": "Already have an account?"
"UNLIMITED_INBOXES": "Unlimited inboxes",
"ROBUST_REPORTING": "Robust Reporting",
"CANNED_RESPONSES": "الردود السريعة",
"AUTO_ASSIGNMENT": "Auto Assignment",
"SECURITY": "Enterprise level security"
}
} }
} }

View File

@ -11,6 +11,7 @@
"OS": "Sistema operatiu", "OS": "Sistema operatiu",
"INITIATED_FROM": "Iniciada des de", "INITIATED_FROM": "Iniciada des de",
"INITIATED_AT": "Iniciada a les", "INITIATED_AT": "Iniciada a les",
"IP_ADDRESS": "Adreça IP",
"CONVERSATIONS": { "CONVERSATIONS": {
"NO_RECORDS_FOUND": "No hi han converses prèvies associades a aquest contacte.", "NO_RECORDS_FOUND": "No hi han converses prèvies associades a aquest contacte.",
"TITLE": "Converses prèvies" "TITLE": "Converses prèvies"

View File

@ -13,8 +13,8 @@
"PLACEHOLDER": "Escriu qualsevol text per cercar missatges", "PLACEHOLDER": "Escriu qualsevol text per cercar missatges",
"NO_MATCHING_RESULTS": "No hi ha missatges que coincideixin amb els paràmetres de cerca." "NO_MATCHING_RESULTS": "No hi ha missatges que coincideixin amb els paràmetres de cerca."
}, },
"UNREAD_MESSAGES": "Unread Messages", "UNREAD_MESSAGES": "Missatges no Llegits",
"UNREAD_MESSAGE": "Unread Message", "UNREAD_MESSAGE": "Missatge no Llegit",
"CLICK_HERE": "Clica aquí", "CLICK_HERE": "Clica aquí",
"LOADING_INBOXES": "S'estan carregant les safates d'entrada", "LOADING_INBOXES": "S'estan carregant les safates d'entrada",
"LOADING_CONVERSATIONS": "S'estan carregant les converses", "LOADING_CONVERSATIONS": "S'estan carregant les converses",
@ -24,6 +24,7 @@
"REPLYING_TO": "Estas responent a:", "REPLYING_TO": "Estas responent a:",
"REMOVE_SELECTION": "Elimina la selecció", "REMOVE_SELECTION": "Elimina la selecció",
"DOWNLOAD": "Descarrega", "DOWNLOAD": "Descarrega",
"UPLOADING_ATTACHMENTS": "Pujant fitxers adjunts...",
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "Resoldre", "RESOLVE_ACTION": "Resoldre",
"REOPEN_ACTION": "Tornar a obrir", "REOPEN_ACTION": "Tornar a obrir",
@ -40,7 +41,10 @@
"PRIVATE_NOTE": "Nota privada", "PRIVATE_NOTE": "Nota privada",
"SEND": "Envia", "SEND": "Envia",
"CREATE": "Afegeix una nota", "CREATE": "Afegeix una nota",
"TWEET": "Tuit" "TWEET": "Tuit",
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files"
}, },
"VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip", "VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip",
"CHANGE_STATUS": "Estat de la conversa canviat", "CHANGE_STATUS": "Estat de la conversa canviat",

View File

@ -42,7 +42,8 @@
"INBOUND_EMAIL_ENABLED": "La continuïtat de converses amb correus electrònics està habilitada per al vostre compte.", "INBOUND_EMAIL_ENABLED": "La continuïtat de converses amb correus electrònics està habilitada per al vostre compte.",
"CUSTOM_EMAIL_DOMAIN_ENABLED": "Ara podeu rebre correus electrònics al vostre domini personalitzat." "CUSTOM_EMAIL_DOMAIN_ENABLED": "Ara podeu rebre correus electrònics al vostre domini personalitzat."
} }
} },
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
}, },
"FORMS": { "FORMS": {
"MULTISELECT": { "MULTISELECT": {

View File

@ -241,7 +241,9 @@
"AUTO_ASSIGNMENT": "Activa l'assignació automàtica", "AUTO_ASSIGNMENT": "Activa l'assignació automàtica",
"INBOX_UPDATE_TITLE": "Configuració de la safata d'entrada", "INBOX_UPDATE_TITLE": "Configuració de la safata d'entrada",
"INBOX_UPDATE_SUB_TEXT": "Actualitza la configuració de la safata d'entrada", "INBOX_UPDATE_SUB_TEXT": "Actualitza la configuració de la safata d'entrada",
"AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses" "AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses",
"HMAC_VERIFICATION": "User Identity Validation",
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
}, },
"FACEBOOK_REAUTHORIZE": { "FACEBOOK_REAUTHORIZE": {
"TITLE": "Reautoritza", "TITLE": "Reautoritza",

View File

@ -1,5 +1,5 @@
/* eslint-disable */
import { default as _agentMgmt } from './agentMgmt.json'; import { default as _agentMgmt } from './agentMgmt.json';
import { default as _labelsMgmt } from './labelsMgmt.json';
import { default as _cannedMgmt } from './cannedMgmt.json'; import { default as _cannedMgmt } from './cannedMgmt.json';
import { default as _chatlist } from './chatlist.json'; import { default as _chatlist } from './chatlist.json';
import { default as _contact } from './contact.json'; import { default as _contact } from './contact.json';
@ -23,6 +23,7 @@ export default {
..._inboxMgmt, ..._inboxMgmt,
..._login, ..._login,
..._report, ..._report,
..._labelsMgmt,
..._resetPassword, ..._resetPassword,
..._setNewPassword, ..._setNewPassword,
..._settings, ..._settings,

View File

@ -3,7 +3,7 @@
"HEADER": "Informes", "HEADER": "Informes",
"LOADING_CHART": "S'estan carregant dades del gràfic...", "LOADING_CHART": "S'estan carregant dades del gràfic...",
"NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.", "NO_ENOUGH_DATA": "No hem rebut suficients punts de dades per generar l'informe. Torneu-ho a provar més endavant.",
"DOWNLOAD_AGENT_REPORTS": "Download agent reports", "DOWNLOAD_AGENT_REPORTS": "Descarregar Informes d'Agent",
"METRICS": { "METRICS": {
"CONVERSATIONS": { "CONVERSATIONS": {
"NAME": "Converses", "NAME": "Converses",

View File

@ -113,7 +113,7 @@
"SIDEBAR": { "SIDEBAR": {
"CONVERSATIONS": "Converses", "CONVERSATIONS": "Converses",
"REPORTS": "Informes", "REPORTS": "Informes",
"CONTACTS": "Contactes (Beta)", "CONTACTS": "Contactes",
"SETTINGS": "Configuracions", "SETTINGS": "Configuracions",
"HOME": "Inici", "HOME": "Inici",
"AGENTS": "Agents", "AGENTS": "Agents",

View File

@ -5,13 +5,18 @@
"TERMS_ACCEPT": "En registrar-vos, esteu dacord amb el nostre <a href=\"https://www.chatwoot.com/terms\">T & C</a> i <a href=\"https://www.chatwoot.com/privacy-policy\">Polítiques de Privadesa</a>", "TERMS_ACCEPT": "En registrar-vos, esteu dacord amb el nostre <a href=\"https://www.chatwoot.com/terms\">T & C</a> i <a href=\"https://www.chatwoot.com/privacy-policy\">Polítiques de Privadesa</a>",
"ACCOUNT_NAME": { "ACCOUNT_NAME": {
"LABEL": "Nom del compte", "LABEL": "Nom del compte",
"PLACEHOLDER": "Wayne Enterprises", "PLACEHOLDER": "Introdueix el nom del compte. ex: Wayne Enterprises",
"ERROR": "El nom del compte és massa curt"
},
"FULL_NAME": {
"LABEL": "Nom complet",
"PLACEHOLDER": "Introdueix el teu nom complet. ex: Bruce Wayne",
"ERROR": "El nom del compte és massa curt" "ERROR": "El nom del compte és massa curt"
}, },
"EMAIL": { "EMAIL": {
"LABEL": "Correu electrònic", "LABEL": "Email de treball",
"PLACEHOLDER": "bruce@wayne.enterprises", "PLACEHOLDER": "Introdueix la teva adreça email de treball. ex: bruce@wayne.enterprises",
"ERROR": "El correu electrònic no és correcte" "ERROR": "Adreça email invàlida"
}, },
"PASSWORD": { "PASSWORD": {
"LABEL": "Contrasenya", "LABEL": "Contrasenya",
@ -28,12 +33,6 @@
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant" "ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
}, },
"SUBMIT": "Envia", "SUBMIT": "Envia",
"FEATURES": { "HAVE_AN_ACCOUNT": "Ja tens un compte?"
"UNLIMITED_INBOXES": "Safates ilimitades",
"ROBUST_REPORTING": "Informa robust",
"CANNED_RESPONSES": "Respostes predeterminades",
"AUTO_ASSIGNMENT": "Tasca Automàtica",
"SECURITY": "Seguretat a nivell empreserial"
}
} }
} }

View File

@ -11,6 +11,7 @@
"OS": "Operační systém", "OS": "Operační systém",
"INITIATED_FROM": "Zahájeno od", "INITIATED_FROM": "Zahájeno od",
"INITIATED_AT": "Zahájeno v", "INITIATED_AT": "Zahájeno v",
"IP_ADDRESS": "IP adresa",
"CONVERSATIONS": { "CONVERSATIONS": {
"NO_RECORDS_FOUND": "K tomuto kontaktu nejsou přiřazeny žádné předchozí konverzace.", "NO_RECORDS_FOUND": "K tomuto kontaktu nejsou přiřazeny žádné předchozí konverzace.",
"TITLE": "Předchozí konverzace" "TITLE": "Předchozí konverzace"

View File

@ -13,8 +13,8 @@
"PLACEHOLDER": "Zadejte jakýkoli text k hledání", "PLACEHOLDER": "Zadejte jakýkoli text k hledání",
"NO_MATCHING_RESULTS": "Vašemu vyhledávání neodpovídají žádné zprávy." "NO_MATCHING_RESULTS": "Vašemu vyhledávání neodpovídají žádné zprávy."
}, },
"UNREAD_MESSAGES": "Unread Messages", "UNREAD_MESSAGES": "Nepřečtené zprávy",
"UNREAD_MESSAGE": "Unread Message", "UNREAD_MESSAGE": "Nepřečtená zpráva",
"CLICK_HERE": "Klikněte zde", "CLICK_HERE": "Klikněte zde",
"LOADING_INBOXES": "Načítání krabic", "LOADING_INBOXES": "Načítání krabic",
"LOADING_CONVERSATIONS": "Načítání konverzací", "LOADING_CONVERSATIONS": "Načítání konverzací",
@ -24,6 +24,7 @@
"REPLYING_TO": "Odpovídáte uživateli:", "REPLYING_TO": "Odpovídáte uživateli:",
"REMOVE_SELECTION": "Odstranit výběr", "REMOVE_SELECTION": "Odstranit výběr",
"DOWNLOAD": "Stáhnout", "DOWNLOAD": "Stáhnout",
"UPLOADING_ATTACHMENTS": "Nahrávání příloh...",
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "Vyřešit", "RESOLVE_ACTION": "Vyřešit",
"REOPEN_ACTION": "Znovu otevřít", "REOPEN_ACTION": "Znovu otevřít",
@ -40,16 +41,19 @@
"PRIVATE_NOTE": "Soukromá poznámka", "PRIVATE_NOTE": "Soukromá poznámka",
"SEND": "Poslat", "SEND": "Poslat",
"CREATE": "Přidat poznámku", "CREATE": "Přidat poznámku",
"TWEET": "Tweet" "TWEET": "Tweet",
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files"
}, },
"VISIBLE_TO_AGENTS": "Soukromá poznámka: Viditelné pouze pro vás a váš tým", "VISIBLE_TO_AGENTS": "Soukromá poznámka: Viditelné pouze pro vás a váš tým",
"CHANGE_STATUS": "Stav konverzace byl změněn", "CHANGE_STATUS": "Stav konverzace byl změněn",
"CHANGE_AGENT": "Konverzace pověřená osoba změněna", "CHANGE_AGENT": "Konverzace pověřená osoba změněna",
"SENT_BY": "Sent by:", "SENT_BY": "Odeslal:",
"ASSIGNMENT": { "ASSIGNMENT": {
"SELECT_AGENT": "Select Agent", "SELECT_AGENT": "Vybrat agenta",
"REMOVE": "Odebrat", "REMOVE": "Odebrat",
"ASSIGN": "Assign" "ASSIGN": "Přiřadit"
} }
}, },
"EMAIL_TRANSCRIPT": { "EMAIL_TRANSCRIPT": {

View File

@ -42,13 +42,14 @@
"INBOUND_EMAIL_ENABLED": "E-mailová konverzace je u vašeho účtu povolena.", "INBOUND_EMAIL_ENABLED": "E-mailová konverzace je u vašeho účtu povolena.",
"CUSTOM_EMAIL_DOMAIN_ENABLED": "Nyní můžete přijímat e-maily na vaši vlastní doménu." "CUSTOM_EMAIL_DOMAIN_ENABLED": "Nyní můžete přijímat e-maily na vaši vlastní doménu."
} }
} },
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
}, },
"FORMS": { "FORMS": {
"MULTISELECT": { "MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select", "ENTER_TO_SELECT": "Stiskněte Enter pro vybrání",
"ENTER_TO_REMOVE": "Press enter to remove", "ENTER_TO_REMOVE": "Stiskněte Enter pro odebrání",
"SELECT_ONE": "Select one" "SELECT_ONE": "Vyberte jeden"
} }
} }
} }

View File

@ -75,10 +75,10 @@
}, },
"REPLY_TIME": { "REPLY_TIME": {
"TITLE": "Nastavit čas odpovědi", "TITLE": "Nastavit čas odpovědi",
"IN_A_FEW_MINUTES": "In a few minutes", "IN_A_FEW_MINUTES": "Do několika minut",
"IN_A_FEW_HOURS": "In a few hours", "IN_A_FEW_HOURS": "Do několika hodin",
"IN_A_DAY": "In a day", "IN_A_DAY": "Do dne",
"HELP_TEXT": "This reply time will be displayed on the live chat widget" "HELP_TEXT": "Tento čas odpovědi bude zobrazen na widgetu"
}, },
"WIDGET_COLOR": { "WIDGET_COLOR": {
"LABEL": "Barva widgetu", "LABEL": "Barva widgetu",
@ -241,7 +241,9 @@
"AUTO_ASSIGNMENT": "Povolit automatické přiřazení", "AUTO_ASSIGNMENT": "Povolit automatické přiřazení",
"INBOX_UPDATE_TITLE": "Nastavení doručené pošty", "INBOX_UPDATE_TITLE": "Nastavení doručené pošty",
"INBOX_UPDATE_SUB_TEXT": "Aktualizujte nastavení doručené pošty", "INBOX_UPDATE_SUB_TEXT": "Aktualizujte nastavení doručené pošty",
"AUTO_ASSIGNMENT_SUB_TEXT": "Povolit nebo zakázat automatické přiřazování nových konverzací agentům přidaným do této schránky." "AUTO_ASSIGNMENT_SUB_TEXT": "Povolit nebo zakázat automatické přiřazování nových konverzací agentům přidaným do této schránky.",
"HMAC_VERIFICATION": "User Identity Validation",
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
}, },
"FACEBOOK_REAUTHORIZE": { "FACEBOOK_REAUTHORIZE": {
"TITLE": "Znovu autorizovat", "TITLE": "Znovu autorizovat",

View File

@ -113,7 +113,7 @@
"SIDEBAR": { "SIDEBAR": {
"CONVERSATIONS": "Konverzace", "CONVERSATIONS": "Konverzace",
"REPORTS": "Zprávy", "REPORTS": "Zprávy",
"CONTACTS": "Contacts (Beta)", "CONTACTS": "Kontakty",
"SETTINGS": "Nastavení", "SETTINGS": "Nastavení",
"HOME": "Home", "HOME": "Home",
"AGENTS": "Agenti", "AGENTS": "Agenti",

View File

@ -5,13 +5,18 @@
"TERMS_ACCEPT": "Registrací souhlasíte s našimi <a href=\"https://www.chatwoot.com/terms\">T & C</a> a <a href=\"https://www.chatwoot.com/privacy-policy\">Zásadami ochrany osobních údajů</a>", "TERMS_ACCEPT": "Registrací souhlasíte s našimi <a href=\"https://www.chatwoot.com/terms\">T & C</a> a <a href=\"https://www.chatwoot.com/privacy-policy\">Zásadami ochrany osobních údajů</a>",
"ACCOUNT_NAME": { "ACCOUNT_NAME": {
"LABEL": "Název účtu", "LABEL": "Název účtu",
"PLACEHOLDER": "Wayne podniky", "PLACEHOLDER": "Enter an account name. eg: Wayne Enterprises",
"ERROR": "Název účtu je příliš krátký" "ERROR": "Account name is too short"
},
"FULL_NAME": {
"LABEL": "Full name",
"PLACEHOLDER": "Enter your full name. eg: Bruce Wayne",
"ERROR": "Full name is too short"
}, },
"EMAIL": { "EMAIL": {
"LABEL": "E-mailová adresa", "LABEL": "Work email",
"PLACEHOLDER": "bruce@wayne.enterprises", "PLACEHOLDER": "Enter your work email address. eg: bruce@wayne.enterprises",
"ERROR": "E-mail je neplatný" "ERROR": "Email address is invalid"
}, },
"PASSWORD": { "PASSWORD": {
"LABEL": "Heslo", "LABEL": "Heslo",
@ -28,12 +33,6 @@
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později" "ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
}, },
"SUBMIT": "Odeslat", "SUBMIT": "Odeslat",
"FEATURES": { "HAVE_AN_ACCOUNT": "Already have an account?"
"UNLIMITED_INBOXES": "Unlimited inboxes",
"ROBUST_REPORTING": "Robust Reporting",
"CANNED_RESPONSES": "Konzervované odpovědi",
"AUTO_ASSIGNMENT": "Auto Assignment",
"SECURITY": "Enterprise level security"
}
} }
} }

View File

@ -11,6 +11,7 @@
"OS": "Operativsystem", "OS": "Operativsystem",
"INITIATED_FROM": "Startet fra", "INITIATED_FROM": "Startet fra",
"INITIATED_AT": "Startet fra", "INITIATED_AT": "Startet fra",
"IP_ADDRESS": "IP Address",
"CONVERSATIONS": { "CONVERSATIONS": {
"NO_RECORDS_FOUND": "Der er ingen tidligere samtaler tilknyttet denne kontakt.", "NO_RECORDS_FOUND": "Der er ingen tidligere samtaler tilknyttet denne kontakt.",
"TITLE": "Tidligere Samtaler" "TITLE": "Tidligere Samtaler"

View File

@ -24,6 +24,7 @@
"REPLYING_TO": "Du svarer til:", "REPLYING_TO": "Du svarer til:",
"REMOVE_SELECTION": "Fjern Markering", "REMOVE_SELECTION": "Fjern Markering",
"DOWNLOAD": "Download", "DOWNLOAD": "Download",
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "Løs", "RESOLVE_ACTION": "Løs",
"REOPEN_ACTION": "Genåben", "REOPEN_ACTION": "Genåben",
@ -40,7 +41,10 @@
"PRIVATE_NOTE": "Privat Note", "PRIVATE_NOTE": "Privat Note",
"SEND": "Send", "SEND": "Send",
"CREATE": "Tilføj Note", "CREATE": "Tilføj Note",
"TWEET": "Tweet" "TWEET": "Tweet",
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files"
}, },
"VISIBLE_TO_AGENTS": "Privat Note: Kun synlig for dig og dit team", "VISIBLE_TO_AGENTS": "Privat Note: Kun synlig for dig og dit team",
"CHANGE_STATUS": "Samtalestatus ændret", "CHANGE_STATUS": "Samtalestatus ændret",

View File

@ -42,7 +42,8 @@
"INBOUND_EMAIL_ENABLED": "Samtale kontinuitet med e-mails er aktiveret for din konto.", "INBOUND_EMAIL_ENABLED": "Samtale kontinuitet med e-mails er aktiveret for din konto.",
"CUSTOM_EMAIL_DOMAIN_ENABLED": "Du kan modtage e-mails på dit brugerdefinerede domæne nu." "CUSTOM_EMAIL_DOMAIN_ENABLED": "Du kan modtage e-mails på dit brugerdefinerede domæne nu."
} }
} },
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
}, },
"FORMS": { "FORMS": {
"MULTISELECT": { "MULTISELECT": {

View File

@ -241,7 +241,9 @@
"AUTO_ASSIGNMENT": "Aktiver automatisk tildeling", "AUTO_ASSIGNMENT": "Aktiver automatisk tildeling",
"INBOX_UPDATE_TITLE": "Indbakke Indstillinger", "INBOX_UPDATE_TITLE": "Indbakke Indstillinger",
"INBOX_UPDATE_SUB_TEXT": "Opdater dine indbakkeindstillinger", "INBOX_UPDATE_SUB_TEXT": "Opdater dine indbakkeindstillinger",
"AUTO_ASSIGNMENT_SUB_TEXT": "Aktiver eller deaktiver automatisk tildeling af nye samtaler til agenter tilføjet til denne indbakke." "AUTO_ASSIGNMENT_SUB_TEXT": "Aktiver eller deaktiver automatisk tildeling af nye samtaler til agenter tilføjet til denne indbakke.",
"HMAC_VERIFICATION": "User Identity Validation",
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
}, },
"FACEBOOK_REAUTHORIZE": { "FACEBOOK_REAUTHORIZE": {
"TITLE": "Genautorisér", "TITLE": "Genautorisér",

View File

@ -113,7 +113,7 @@
"SIDEBAR": { "SIDEBAR": {
"CONVERSATIONS": "Samtaler", "CONVERSATIONS": "Samtaler",
"REPORTS": "Rapporter", "REPORTS": "Rapporter",
"CONTACTS": "Kontakter (Beta)", "CONTACTS": "Kontakter",
"SETTINGS": "Indstillinger", "SETTINGS": "Indstillinger",
"HOME": "Hjem", "HOME": "Hjem",
"AGENTS": "Agenter", "AGENTS": "Agenter",

View File

@ -5,13 +5,18 @@
"TERMS_ACCEPT": "Ved at tilmelde dig, accepterer du vores <a href=\"https://www.chatwoot.com/terms\">T & C</a> og <a href=\"https://www.chatwoot.com/privacy-policy\">Privatlivspolitik</a>", "TERMS_ACCEPT": "Ved at tilmelde dig, accepterer du vores <a href=\"https://www.chatwoot.com/terms\">T & C</a> og <a href=\"https://www.chatwoot.com/privacy-policy\">Privatlivspolitik</a>",
"ACCOUNT_NAME": { "ACCOUNT_NAME": {
"LABEL": "Kontonavn", "LABEL": "Kontonavn",
"PLACEHOLDER": "Wayne Enterprises", "PLACEHOLDER": "Enter an account name. eg: Wayne Enterprises",
"ERROR": "Kontonavn er for kort" "ERROR": "Account name is too short"
},
"FULL_NAME": {
"LABEL": "Full name",
"PLACEHOLDER": "Enter your full name. eg: Bruce Wayne",
"ERROR": "Full name is too short"
}, },
"EMAIL": { "EMAIL": {
"LABEL": "E-mail", "LABEL": "Work email",
"PLACEHOLDER": "bruce@wayne.enterprises", "PLACEHOLDER": "Enter your work email address. eg: bruce@wayne.enterprises",
"ERROR": "E-mail er ugyldig" "ERROR": "Email address is invalid"
}, },
"PASSWORD": { "PASSWORD": {
"LABEL": "Adgangskode", "LABEL": "Adgangskode",
@ -28,12 +33,6 @@
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere" "ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere"
}, },
"SUBMIT": "Send", "SUBMIT": "Send",
"FEATURES": { "HAVE_AN_ACCOUNT": "Already have an account?"
"UNLIMITED_INBOXES": "Unlimited inboxes",
"ROBUST_REPORTING": "Robust Reporting",
"CANNED_RESPONSES": "Standardsvar Svar",
"AUTO_ASSIGNMENT": "Auto Assignment",
"SECURITY": "Enterprise level security"
}
} }
} }

View File

@ -77,8 +77,8 @@
"CONTENT": "hat eine URL geteilt" "CONTENT": "hat eine URL geteilt"
} }
}, },
"RECEIVED_VIA_EMAIL": "Received via email", "RECEIVED_VIA_EMAIL": "Per E-Mail empfangen",
"VIEW_TWEET_IN_TWITTER": "View tweet in Twitter", "VIEW_TWEET_IN_TWITTER": "Tweet auf Twitter anzeigen",
"REPLY_TO_TWEET": "Reply to this tweet" "REPLY_TO_TWEET": "Auf diesen Tweet antworten"
} }
} }

View File

@ -1,113 +1,114 @@
{ {
"CONTACT_PANEL": { "CONTACT_PANEL": {
"NOT_AVAILABLE": "Not Available", "NOT_AVAILABLE": "Nicht verfügbar",
"EMAIL_ADDRESS": "E-Mail-Addresse", "EMAIL_ADDRESS": "E-Mail-Addresse",
"PHONE_NUMBER": "Telefonnummer", "PHONE_NUMBER": "Telefonnummer",
"COPY_SUCCESSFUL": "Copied to clipboard successfully", "COPY_SUCCESSFUL": "Der Code wurde erfolgreich in die Zwischenablage kopiert",
"COMPANY": "Company", "COMPANY": "Firma",
"LOCATION": "Ort", "LOCATION": "Ort",
"CONVERSATION_TITLE": "Unterhaltungsdetails", "CONVERSATION_TITLE": "Unterhaltungsdetails",
"BROWSER": "Browser", "BROWSER": "Browser",
"OS": "Betriebssystem", "OS": "Betriebssystem",
"INITIATED_FROM": "Initiiert von", "INITIATED_FROM": "Initiiert von",
"INITIATED_AT": "Initiiert bei", "INITIATED_AT": "Initiiert bei",
"IP_ADDRESS": "IP-Adresse",
"CONVERSATIONS": { "CONVERSATIONS": {
"NO_RECORDS_FOUND": "Es sind keine vorherigen Gespräche mit diesem Kontakt verbunden.", "NO_RECORDS_FOUND": "Es sind keine vorherigen Gespräche mit diesem Kontakt verbunden.",
"TITLE": "Vorherige Gespräche" "TITLE": "Vorherige Gespräche"
}, },
"CUSTOM_ATTRIBUTES": { "CUSTOM_ATTRIBUTES": {
"TITLE": "Custom Attributes" "TITLE": "Benutzerdefinierte Attribute"
}, },
"LABELS": { "LABELS": {
"TITLE": "Konversationsetiketten", "TITLE": "Konversationsetiketten",
"MODAL": { "MODAL": {
"TITLE": "Labels for", "TITLE": "Labels für",
"ACTIVE_LABELS": "Labels added to the conversation", "ACTIVE_LABELS": "Labels zur Unterhaltung hinzugefügt",
"INACTIVE_LABELS": "Labels available in the account", "INACTIVE_LABELS": "Verfügbare Labels im Konto",
"REMOVE": "Click on X icon to remove the label", "REMOVE": "Klicken Sie auf das X-Symbol, um das Label zu entfernen",
"ADD": "Click on + icon to add the label", "ADD": "Klicken Sie auf das + Symbol, um ein Label hinzuzufügen",
"UPDATE_BUTTON": "Update labels", "UPDATE_BUTTON": "Labels aktualisieren",
"UPDATE_ERROR": "Etiketten konnten nicht aktualisiert werden. Versuchen Sie es erneut." "UPDATE_ERROR": "Etiketten konnten nicht aktualisiert werden. Versuchen Sie es erneut."
}, },
"NO_LABELS_TO_ADD": "There are no more labels defined in the account.", "NO_LABELS_TO_ADD": "Es sind keine weiteren Labels im Konto definiert.",
"NO_AVAILABLE_LABELS": "There are no labels added to this conversation." "NO_AVAILABLE_LABELS": "Zu dieser Unterhaltung wurden noch keine Labels hinzugefügt."
}, },
"MUTE_CONTACT": "Mute Conversation", "MUTE_CONTACT": "Unterhaltung stummschalten",
"UNMUTE_CONTACT": "Unmute Conversation", "UNMUTE_CONTACT": "Unterhaltung entmuten",
"MUTED_SUCCESS": "This conversation is muted for 6 hours", "MUTED_SUCCESS": "Diese Unterhaltung ist für 6 Stunden auf stumm schalten",
"UNMUTED_SUCCESS": "This conversation is unmuted", "UNMUTED_SUCCESS": "Diese Unterhaltung ist nicht mehr stumm geschaltet",
"SEND_TRANSCRIPT": "Send Transcript", "SEND_TRANSCRIPT": "Transkript senden",
"EDIT_LABEL": "Bearbeiten" "EDIT_LABEL": "Bearbeiten"
}, },
"EDIT_CONTACT": { "EDIT_CONTACT": {
"BUTTON_LABEL": "Edit Contact", "BUTTON_LABEL": "Kontakt bearbeiten",
"TITLE": "Edit contact", "TITLE": "Kontakt bearbeiten",
"DESC": "Edit contact details", "DESC": "Kontaktdetails bearbeiten",
"FORM": { "FORM": {
"SUBMIT": "Einreichen", "SUBMIT": "Einreichen",
"CANCEL": "Stornieren", "CANCEL": "Stornieren",
"AVATAR": { "AVATAR": {
"LABEL": "Contact Avatar" "LABEL": "Kontaktbild"
}, },
"NAME": { "NAME": {
"PLACEHOLDER": "Enter the full name of the contact", "PLACEHOLDER": "Vollständigen Namen des Kontakts eingeben",
"LABEL": "Full Name" "LABEL": "Vollständiger Name"
}, },
"BIO": { "BIO": {
"PLACEHOLDER": "Enter the bio of the contact", "PLACEHOLDER": "Beschreibung dieses Kontakts",
"LABEL": "Bio" "LABEL": "Beschreibung"
}, },
"EMAIL_ADDRESS": { "EMAIL_ADDRESS": {
"PLACEHOLDER": "Enter the email address of the contact", "PLACEHOLDER": "Geben Sie die E-Mail-Adresse des Kontakts ein",
"LABEL": "E-Mail-Addresse" "LABEL": "E-Mail-Addresse"
}, },
"PHONE_NUMBER": { "PHONE_NUMBER": {
"PLACEHOLDER": "Enter the phone number of the contact", "PLACEHOLDER": "Geben Sie die Telefonnummer des Kontakts ein",
"LABEL": "Phone Number" "LABEL": "Telefonnummer"
}, },
"LOCATION": { "LOCATION": {
"PLACEHOLDER": "Enter the location of the contact", "PLACEHOLDER": "Geben Sie den Standort des Kontakts ein",
"LABEL": "Ort" "LABEL": "Ort"
}, },
"COMPANY_NAME": { "COMPANY_NAME": {
"PLACEHOLDER": "Enter the company name", "PLACEHOLDER": "Firmenname eingeben",
"LABEL": "Company Name" "LABEL": "Firmenname"
}, },
"SOCIAL_PROFILES": { "SOCIAL_PROFILES": {
"FACEBOOK": { "FACEBOOK": {
"PLACEHOLDER": "Enter the Facebook username", "PLACEHOLDER": "Facebook-Benutzername eingeben",
"LABEL": "Facebook" "LABEL": "Facebook"
}, },
"TWITTER": { "TWITTER": {
"PLACEHOLDER": "Enter the Twitter username", "PLACEHOLDER": "Twitter-Benutzername eingeben",
"LABEL": "Twitter" "LABEL": "Twitter"
}, },
"LINKEDIN": { "LINKEDIN": {
"PLACEHOLDER": "Enter the LinkedIn username", "PLACEHOLDER": "Geben Sie den LinkedIn-Benutzernamen ein",
"LABEL": "LinkedIn" "LABEL": "LinkedIn"
}, },
"GITHUB": { "GITHUB": {
"PLACEHOLDER": "Enter the Github username", "PLACEHOLDER": "Gitub-Benutzernamen eingeben",
"LABEL": "Github" "LABEL": "Github"
} }
} }
}, },
"SUCCESS_MESSAGE": "Updated contact successfully", "SUCCESS_MESSAGE": "Kontakt erfolgreich aktualisiert",
"CONTACT_ALREADY_EXIST": "This email address is in use for another contact.", "CONTACT_ALREADY_EXIST": "Diese E-Mail-Adresse wird bereits für einen anderen Kontakt verwendet.",
"ERROR_MESSAGE": "There was an error updating the contact, please try again" "ERROR_MESSAGE": "Beim Aktualisieren des Kontakts ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut"
}, },
"CONTACTS_PAGE": { "CONTACTS_PAGE": {
"HEADER": "Contacts", "HEADER": "Kontakte",
"SEARCH_BUTTON": "Search", "SEARCH_BUTTON": "Suchen",
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts", "SEARCH_INPUT_PLACEHOLDER": "Suche nach Kontakten",
"LIST": { "LIST": {
"LOADING_MESSAGE": "Loading contacts...", "LOADING_MESSAGE": "Kontakte werden geladen...",
"404": "No contacts matches your search 🔍", "404": "Keine Kontakte entsprechend Deiner Suche gefunden 🔍",
"TABLE_HEADER": [ "TABLE_HEADER": [
"Name", "Name",
"Phone Number", "Telefonnummer",
"Gespräche", "Gespräche",
"Last Contacted" "Letzter Kontakt"
] ]
} }
} }

View File

@ -6,24 +6,25 @@
"NO_INBOX_1": "Hallo! Sieht so aus, als hätten Sie noch keine Posteingänge hinzugefügt.", "NO_INBOX_1": "Hallo! Sieht so aus, als hätten Sie noch keine Posteingänge hinzugefügt.",
"NO_INBOX_2": " um loszulegen", "NO_INBOX_2": " um loszulegen",
"NO_INBOX_AGENT": "Oh oh! Sieht so aus, als wären Sie nicht Teil eines Posteingangs. Bitte wenden Sie sich an Ihren Administrator", "NO_INBOX_AGENT": "Oh oh! Sieht so aus, als wären Sie nicht Teil eines Posteingangs. Bitte wenden Sie sich an Ihren Administrator",
"SEARCH_MESSAGES": "Search for messages in conversations", "SEARCH_MESSAGES": "Nachrichten durchsuchen",
"SEARCH": { "SEARCH": {
"TITLE": "Search messages", "TITLE": "Nachrichten durchsuchen",
"LOADING_MESSAGE": "Crunching data...", "LOADING_MESSAGE": "Daten werden geladen...",
"PLACEHOLDER": "Type any text to search messages", "PLACEHOLDER": "Geben Sie einen Text ein, um danach zu suchen",
"NO_MATCHING_RESULTS": "There are no messages matching the search parameters." "NO_MATCHING_RESULTS": "Keine passenden Nachrichten gefunden."
}, },
"UNREAD_MESSAGES": "Unread Messages", "UNREAD_MESSAGES": "Ungelesene Nachrichten",
"UNREAD_MESSAGE": "Unread Message", "UNREAD_MESSAGE": "Ungelesene Nachricht",
"CLICK_HERE": "Hier klicken", "CLICK_HERE": "Hier klicken",
"LOADING_INBOXES": "Posteingänge laden", "LOADING_INBOXES": "Posteingänge laden",
"LOADING_CONVERSATIONS": "Gespräche laden", "LOADING_CONVERSATIONS": "Gespräche laden",
"CANNOT_REPLY": "You cannot reply due to", "CANNOT_REPLY": "Du kannst nicht Antworten aufgrund von",
"24_HOURS_WINDOW": "24 hour message window restriction", "24_HOURS_WINDOW": "24-Stunden-Nachrichtenfenster-Beschränkung",
"LAST_INCOMING_TWEET": "You are replying to the last incoming tweet", "LAST_INCOMING_TWEET": "Du antwortest auf den letzten eingehenden Tweet",
"REPLYING_TO": "You are replying to:", "REPLYING_TO": "Du antwortest auf:",
"REMOVE_SELECTION": "Remove Selection", "REMOVE_SELECTION": "Auswahl entfernen",
"DOWNLOAD": "Herunterladen", "DOWNLOAD": "Herunterladen",
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "Fall schließen", "RESOLVE_ACTION": "Fall schließen",
"REOPEN_ACTION": "Wieder öffnen", "REOPEN_ACTION": "Wieder öffnen",
@ -40,31 +41,34 @@
"PRIVATE_NOTE": "Private Notiz", "PRIVATE_NOTE": "Private Notiz",
"SEND": "Senden", "SEND": "Senden",
"CREATE": "Notiz hinzufügen", "CREATE": "Notiz hinzufügen",
"TWEET": "Tweet" "TWEET": "Tweet",
"TIP_FORMAT_ICON": "Show rich text editor",
"TIP_EMOJI_ICON": "Show emoji selector",
"TIP_ATTACH_ICON": "Attach files"
}, },
"VISIBLE_TO_AGENTS": "Privater Hinweis: Nur für Sie und Ihr Team sichtbar", "VISIBLE_TO_AGENTS": "Privater Hinweis: Nur für Sie und Ihr Team sichtbar",
"CHANGE_STATUS": "Gesprächsstatus geändert", "CHANGE_STATUS": "Gesprächsstatus geändert",
"CHANGE_AGENT": "Konversationsempfänger geändert", "CHANGE_AGENT": "Konversationsempfänger geändert",
"SENT_BY": "Sent by:", "SENT_BY": "Gesendet von:",
"ASSIGNMENT": { "ASSIGNMENT": {
"SELECT_AGENT": "Select Agent", "SELECT_AGENT": "Agent auswählen",
"REMOVE": "Entfernen", "REMOVE": "Entfernen",
"ASSIGN": "Assign" "ASSIGN": "Zuordnen"
} }
}, },
"EMAIL_TRANSCRIPT": { "EMAIL_TRANSCRIPT": {
"TITLE": "Send conversation transcript", "TITLE": "Konversations-Transkript senden",
"DESC": "Send a copy of the conversation transcript to the specified email address", "DESC": "Kopie des Konversationsprotokolls an die angegebene E-Mail-Adresse senden",
"SUBMIT": "Einreichen", "SUBMIT": "Einreichen",
"CANCEL": "Stornieren", "CANCEL": "Stornieren",
"SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully", "SEND_EMAIL_SUCCESS": "Das Chat-Protokoll wurde erfolgreich gesendet",
"SEND_EMAIL_ERROR": "There was an error, please try again", "SEND_EMAIL_ERROR": "Es ist ein Fehler aufgetreten, bitte versuche es erneut",
"FORM": { "FORM": {
"SEND_TO_CONTACT": "Send the transcript to the customer", "SEND_TO_CONTACT": "Das Transkript an den Kunden senden",
"SEND_TO_AGENT": "Send the transcript to the assigned agent", "SEND_TO_AGENT": "Transkript an den zugewiesenen Agent senden",
"SEND_TO_OTHER_EMAIL_ADDRESS": "Send the transcript to another email address", "SEND_TO_OTHER_EMAIL_ADDRESS": "Transkript an eine andere E-Mail-Adresse senden",
"EMAIL": { "EMAIL": {
"PLACEHOLDER": "Enter an email address", "PLACEHOLDER": "E-Mail-Adresse eingeben",
"ERROR": "Bitte geben Sie eine gültige E-Mail-Adresse ein" "ERROR": "Bitte geben Sie eine gültige E-Mail-Adresse ein"
} }
} }

View File

@ -2,7 +2,7 @@
"GENERAL_SETTINGS": { "GENERAL_SETTINGS": {
"TITLE": "Kontoeinstellungen", "TITLE": "Kontoeinstellungen",
"SUBMIT": "Update Einstellungen", "SUBMIT": "Update Einstellungen",
"BACK": "Back", "BACK": "Zurück",
"UPDATE": { "UPDATE": {
"ERROR": "Einstellungen konnten nicht aktualisiert werden, versuchen Sie es erneut!", "ERROR": "Einstellungen konnten nicht aktualisiert werden, versuchen Sie es erneut!",
"SUCCESS": "Kontoeinstellungen erfolgreich aktualisiert" "SUCCESS": "Kontoeinstellungen erfolgreich aktualisiert"
@ -24,31 +24,32 @@
"ERROR": "" "ERROR": ""
}, },
"DOMAIN": { "DOMAIN": {
"LABEL": "Incoming Email Domain", "LABEL": "Eingehende E-Mail-Domain",
"PLACEHOLDER": "The domain where you will receive the emails", "PLACEHOLDER": "Die Domain, von der E-Mails empfangen werden",
"ERROR": "" "ERROR": ""
}, },
"SUPPORT_EMAIL": { "SUPPORT_EMAIL": {
"LABEL": "Support Email", "LABEL": "Support-E-Mail",
"PLACEHOLDER": "Your company's support email", "PLACEHOLDER": "Support E-Mail deines Unternehmens",
"ERROR": "" "ERROR": ""
}, },
"AUTO_RESOLVE_DURATION": { "AUTO_RESOLVE_DURATION": {
"LABEL": "Number of days after a ticket should auto resolve if there is no activity", "LABEL": "Anzahl der Tage, nach denen ein Ticket automatisch geschlossen wird, wenn keine Aktivität erfolgt",
"PLACEHOLDER": "30", "PLACEHOLDER": "30",
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)" "ERROR": "Bitte gebe eine gültige automatische Auflösungsdauer ein (mindestens 1 Tag)"
}, },
"FEATURES": { "FEATURES": {
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.", "INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",
"CUSTOM_EMAIL_DOMAIN_ENABLED": "You can receive emails in your custom domain now." "CUSTOM_EMAIL_DOMAIN_ENABLED": "Du kannst E-Mails jetzt von der festgelegten Domain erhalten."
} }
} },
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
}, },
"FORMS": { "FORMS": {
"MULTISELECT": { "MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select", "ENTER_TO_SELECT": "Drücke Enter zum Auswählen",
"ENTER_TO_REMOVE": "Press enter to remove", "ENTER_TO_REMOVE": "Drücke Enter zum Entfernen",
"SELECT_ONE": "Select one" "SELECT_ONE": "Eine Option wählen"
} }
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"INBOX_MGMT": { "INBOX_MGMT": {
"HEADER": "Posteingänge", "HEADER": "Posteingänge",
"SIDEBAR_TXT": "<p><b>Inbox</b></p> <p> When you connect a website or a facebook Page to Chatwoot, it is called an <b>Inbox</b>. You can have unlimited inboxes in your Chatwoot account. </p><p> Click on <b>Add Inbox</b> to connect a website or a Facebook Page. </p><p> In the Dashboard, you can see all the conversations from all your inboxes in a single place and respond to them under the `Conversations` tab. </p><p> You can also see conversations specific to an inbox by clicking on the inbox name on the left pane of the dashboard. </p>", "SIDEBAR_TXT": "<p><b>Posteingang</b></p> <p> Wenn du eine Webseite oder eine Facebook-Seite mit Chatwood verbindest, wird dieser Kanal als <b>Posteingang</b> bezeichnet. Dabei können unbegrenzte viele Posteingänge angelegt werden. </p><p> Klicke auf <b>Posteingang hinzufügen</b>, um eine Webseite oder eine Facebook-Seite zu verbinden. </p><p> Im Dashboard du kannst alle Unterhaltungen aus allen deinen Posteingängen an einem Ort sehen und unter der Registerkarte \"Unterhaltungen\" beantworten. </p><p> Du kannst dir auch nur Konversationen anzeigen lassen, die in einen bestimmten Posteingang enthalten sind, indem du auf den Posteingangsnamen auf der linken Seite des Dashboards klicken. </p>",
"LIST": { "LIST": {
"404": "Diesem Konto sind keine Posteingänge zugeordnet." "404": "Diesem Konto sind keine Posteingänge zugeordnet."
}, },
@ -30,12 +30,12 @@
"ADD": { "ADD": {
"FB": { "FB": {
"HELP": "PS: Durch die Anmeldung erhalten wir nur Zugriff auf die Nachrichten Ihrer Seite. Auf Ihre privaten Nachrichten kann Chatwoot niemals zugreifen.", "HELP": "PS: Durch die Anmeldung erhalten wir nur Zugriff auf die Nachrichten Ihrer Seite. Auf Ihre privaten Nachrichten kann Chatwoot niemals zugreifen.",
"CHOOSE_PAGE": "Choose Page", "CHOOSE_PAGE": "Seite auswählen",
"CHOOSE_PLACEHOLDER": "Select a page from the list", "CHOOSE_PLACEHOLDER": "Wähle eine Seite aus der Liste",
"INBOX_NAME": "Inbox Name", "INBOX_NAME": "Name des Posteingang",
"ADD_NAME": "Add a name for your inbox", "ADD_NAME": "Namen für diesen Posteingang eingeben",
"PICK_NAME": "Pick A Name Your Inbox", "PICK_NAME": "Wähle einen Namen für deinen Posteingang",
"PICK_A_VALUE": "Pick a value" "PICK_A_VALUE": "Wähle einen Wert"
}, },
"TWITTER": { "TWITTER": {
"HELP": "Um Ihr Twitter-Profil als Kanal hinzuzufügen, müssen Sie Ihr Twitter-Profil authentifizieren, indem Sie auf 'Mit Twitter anmelden' klicken." "HELP": "Um Ihr Twitter-Profil als Kanal hinzuzufügen, müssen Sie Ihr Twitter-Profil authentifizieren, indem Sie auf 'Mit Twitter anmelden' klicken."
@ -43,7 +43,7 @@
"WEBSITE_CHANNEL": { "WEBSITE_CHANNEL": {
"TITLE": "Website-Kanal", "TITLE": "Website-Kanal",
"DESC": "Erstellen Sie einen Kanal für Ihre Website und unterstützen Sie Ihre Kunden über unser Website-Widget.", "DESC": "Erstellen Sie einen Kanal für Ihre Website und unterstützen Sie Ihre Kunden über unser Website-Widget.",
"LOADING_MESSAGE": "Creating Website Support Channel", "LOADING_MESSAGE": "Website-Support-Channel erstellen",
"CHANNEL_AVATAR": { "CHANNEL_AVATAR": {
"LABEL": "Channel Avatar" "LABEL": "Channel Avatar"
}, },
@ -52,33 +52,33 @@
"PLACEHOLDER": "Geben Sie den Namen Ihrer Website ein (eg: Acme Inc)" "PLACEHOLDER": "Geben Sie den Namen Ihrer Website ein (eg: Acme Inc)"
}, },
"CHANNEL_DOMAIN": { "CHANNEL_DOMAIN": {
"LABEL": "Website Domain", "LABEL": "Website-Domain",
"PLACEHOLDER": "Geben Sie Ihre Website-Domain ein (eg: acme.com)" "PLACEHOLDER": "Geben Sie Ihre Website-Domain ein (eg: acme.com)"
}, },
"CHANNEL_WELCOME_TITLE": { "CHANNEL_WELCOME_TITLE": {
"LABEL": "Welcome Heading", "LABEL": "Willkommens-Überschrift",
"PLACEHOLDER": "Hi there !" "PLACEHOLDER": "Hallo!"
}, },
"CHANNEL_WELCOME_TAGLINE": { "CHANNEL_WELCOME_TAGLINE": {
"LABEL": "Welcome Tagline", "LABEL": "Willkommens-Schlagzeile",
"PLACEHOLDER": "We make it simple to connect with us. Ask us anything, or share your feedback." "PLACEHOLDER": "Wir machen es einfach, mit uns in Verbindung zu treten. Fragen Sie uns etwas oder teilen Sie Ihr Feedback."
}, },
"CHANNEL_GREETING_MESSAGE": { "CHANNEL_GREETING_MESSAGE": {
"LABEL": "Channel greeting message", "LABEL": "Grußnachricht des Kanals",
"PLACEHOLDER": "Acme Inc typically replies in a few hours." "PLACEHOLDER": "Wir antworten in der Regel in wenigen Stunden."
}, },
"CHANNEL_GREETING_TOGGLE": { "CHANNEL_GREETING_TOGGLE": {
"LABEL": "Enable channel greeting", "LABEL": "Kanal Begrüßung aktivieren",
"HELP_TEXT": "Send a greeting message to the user when he starts the conversation.", "HELP_TEXT": "Senden Sie eine Grußnachricht an den Benutzer, wenn er die Unterhaltung beginnt.",
"ENABLED": "Aktiviert", "ENABLED": "Aktiviert",
"DISABLED": "Behindert" "DISABLED": "Behindert"
}, },
"REPLY_TIME": { "REPLY_TIME": {
"TITLE": "Reaktionszeit festlegen", "TITLE": "Reaktionszeit festlegen",
"IN_A_FEW_MINUTES": "In a few minutes", "IN_A_FEW_MINUTES": "Innerhalb weniger Minuten",
"IN_A_FEW_HOURS": "In a few hours", "IN_A_FEW_HOURS": "Innerhalb weniger Stunden",
"IN_A_DAY": "In a day", "IN_A_DAY": "Innerhalb eines Tages",
"HELP_TEXT": "This reply time will be displayed on the live chat widget" "HELP_TEXT": "Diese Antwortzeit wird im Live-Chat-Widget angezeigt"
}, },
"WIDGET_COLOR": { "WIDGET_COLOR": {
"LABEL": "Widget Farbe", "LABEL": "Widget Farbe",
@ -87,7 +87,7 @@
"SUBMIT_BUTTON": "Posteingang erstellen" "SUBMIT_BUTTON": "Posteingang erstellen"
}, },
"TWILIO": { "TWILIO": {
"TITLE": "Twilio SMS/Whatsapp Channel", "TITLE": "Twilio SMS/Whatsapp Kanal",
"DESC": "Integrieren Sie Twilio und unterstützen Sie Ihre Kunden per SMS/Whatsapp.", "DESC": "Integrieren Sie Twilio und unterstützen Sie Ihre Kunden per SMS/Whatsapp.",
"ACCOUNT_SID": { "ACCOUNT_SID": {
"LABEL": "Account SID", "LABEL": "Account SID",
@ -95,11 +95,11 @@
"ERROR": "Dieses Feld wird benötigt" "ERROR": "Dieses Feld wird benötigt"
}, },
"CHANNEL_TYPE": { "CHANNEL_TYPE": {
"LABEL": "Channel Type", "LABEL": "Kanal-Typ",
"ERROR": "Please select your Channel Type" "ERROR": "Bitte wählen Sie den Kanal-Typ aus"
}, },
"AUTH_TOKEN": { "AUTH_TOKEN": {
"LABEL": "Auth Token", "LABEL": "Auth-Token",
"PLACEHOLDER": "Bitte geben Sie Ihr Twilio Auth Token ein", "PLACEHOLDER": "Bitte geben Sie Ihr Twilio Auth Token ein",
"ERROR": "Dieses Feld wird benötigt" "ERROR": "Dieses Feld wird benötigt"
}, },
@ -115,7 +115,7 @@
}, },
"API_CALLBACK": { "API_CALLBACK": {
"TITLE": "Callback URL", "TITLE": "Callback URL",
"SUBTITLE": "You have to configure the message callback URL in Twilio with the URL mentioned here." "SUBTITLE": "Sie müssen die Callback-URL in Twilio mit der hier genannten URL konfigurieren."
}, },
"SUBMIT_BUTTON": "Erstellen Sie Twilio Channel", "SUBMIT_BUTTON": "Erstellen Sie Twilio Channel",
"API": { "API": {
@ -123,8 +123,8 @@
} }
}, },
"API_CHANNEL": { "API_CHANNEL": {
"TITLE": "API Channel", "TITLE": "API-Kanal",
"DESC": "Integrate with API channel and start supporting your customers.", "DESC": "Integrieren Sie einen API-Kanal und starten Sie mit der Unterstützung Ihrer Kunden.",
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "Kanal Name", "LABEL": "Kanal Name",
"PLACEHOLDER": "Bitte geben Sie einen Kanalnamen ein", "PLACEHOLDER": "Bitte geben Sie einen Kanalnamen ein",
@ -132,32 +132,32 @@
}, },
"WEBHOOK_URL": { "WEBHOOK_URL": {
"LABEL": "Webhook-URL", "LABEL": "Webhook-URL",
"SUBTITLE": "Configure the URL where you want to recieve callbacks on events.", "SUBTITLE": "Konfigurieren Sie die URL, auf der Sie Callbacks bei Events erhalten möchten.",
"PLACEHOLDER": "Webhook-URL" "PLACEHOLDER": "Webhook-URL"
}, },
"SUBMIT_BUTTON": "Create API Channel", "SUBMIT_BUTTON": "API-Kanal erstellen",
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the api channel" "ERROR_MESSAGE": "Der API-Kanal konnte nicht gespeichert werden"
} }
}, },
"EMAIL_CHANNEL": { "EMAIL_CHANNEL": {
"TITLE": "Email Channel", "TITLE": "E-Mail-Kanal",
"DESC": "Integrate you email inbox.", "DESC": "Integrieren Sie Ihren Posteingang.",
"CHANNEL_NAME": { "CHANNEL_NAME": {
"LABEL": "Kanal Name", "LABEL": "Kanal Name",
"PLACEHOLDER": "Bitte geben Sie einen Kanalnamen ein", "PLACEHOLDER": "Bitte geben Sie einen Kanalnamen ein",
"ERROR": "Dieses Feld wird benötigt" "ERROR": "Dieses Feld wird benötigt"
}, },
"EMAIL": { "EMAIL": {
"LABEL": "Email", "LABEL": "E-Mail",
"SUBTITLE": "Email where your customers sends you support tickets", "SUBTITLE": "E-Mail Adresse, an die Ihre Kunden Ihnen Support-Tickets senden",
"PLACEHOLDER": "Email" "PLACEHOLDER": "E-Mail"
}, },
"SUBMIT_BUTTON": "Create Email Channel", "SUBMIT_BUTTON": "E-Mail-Kanal erstellen",
"API": { "API": {
"ERROR_MESSAGE": "We were not able to save the email channel" "ERROR_MESSAGE": "Wir konnten den E-Mail-Kanal nicht speichern"
}, },
"FINISH_MESSAGE": "Start forwarding your emails to the following email address." "FINISH_MESSAGE": "Starten Sie die Weiterleitung Ihrer E-Mails an die folgende E-Mail-Adresse."
}, },
"AUTH": { "AUTH": {
"TITLE": "Kanäle", "TITLE": "Kanäle",
@ -166,8 +166,8 @@
"AGENTS": { "AGENTS": {
"TITLE": "Agenten", "TITLE": "Agenten",
"DESC": "Hier können Sie Agenten hinzufügen, um Ihren neu erstellten Posteingang zu verwalten. Nur diese ausgewählten Agenten haben Zugriff auf Ihren Posteingang. Agenten, die nicht Teil dieses Posteingangs sind, können bei der Anmeldung keine Nachrichten in diesem Posteingang sehen oder darauf antworten. <br> <b> PS: </b> Wenn Sie als Administrator Zugriff auf alle Posteingänge benötigen, sollten Sie sich als Agent zu allen von Ihnen erstellten Posteingängen hinzufügen.", "DESC": "Hier können Sie Agenten hinzufügen, um Ihren neu erstellten Posteingang zu verwalten. Nur diese ausgewählten Agenten haben Zugriff auf Ihren Posteingang. Agenten, die nicht Teil dieses Posteingangs sind, können bei der Anmeldung keine Nachrichten in diesem Posteingang sehen oder darauf antworten. <br> <b> PS: </b> Wenn Sie als Administrator Zugriff auf alle Posteingänge benötigen, sollten Sie sich als Agent zu allen von Ihnen erstellten Posteingängen hinzufügen.",
"VALIDATION_ERROR": "Add atleast one agent to your new Inbox", "VALIDATION_ERROR": "Fügen Sie mindestens einen Agenten zu Ihrem neuen Posteingang hinzu",
"PICK_AGENTS": "Pick agents for the inbox" "PICK_AGENTS": "Agenten für den Posteingang auswählen"
}, },
"DETAILS": { "DETAILS": {
"TITLE": "Posteingangsdetails", "TITLE": "Posteingangsdetails",
@ -223,14 +223,14 @@
}, },
"TABS": { "TABS": {
"SETTINGS": "die Einstellungen", "SETTINGS": "die Einstellungen",
"COLLABORATORS": "Collaborators", "COLLABORATORS": "Mitarbeitende",
"CONFIGURATION": "Configuration" "CONFIGURATION": "Konfiguration"
}, },
"SETTINGS": "die Einstellungen", "SETTINGS": "die Einstellungen",
"FEATURES": { "FEATURES": {
"LABEL": "Features", "LABEL": "Funktionen",
"DISPLAY_FILE_PICKER": "Display file picker on the widget", "DISPLAY_FILE_PICKER": "Dateiauswahl im Widget anzeigen",
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget" "DISPLAY_EMOJI_PICKER": "Emoji-Auswahl im Widget anzeigen"
}, },
"SETTINGS_POPUP": { "SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger-Skript", "MESSENGER_HEADING": "Messenger-Skript",
@ -239,15 +239,17 @@
"INBOX_AGENTS_SUB_TEXT": "Hinzufügen oder Entfernen von Agenten zu diesem Posteingang", "INBOX_AGENTS_SUB_TEXT": "Hinzufügen oder Entfernen von Agenten zu diesem Posteingang",
"UPDATE": "Aktualisieren", "UPDATE": "Aktualisieren",
"AUTO_ASSIGNMENT": "Aktivieren Sie die automatische Zuweisung", "AUTO_ASSIGNMENT": "Aktivieren Sie die automatische Zuweisung",
"INBOX_UPDATE_TITLE": "Inbox Settings", "INBOX_UPDATE_TITLE": "Posteingang Einstellungen",
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings", "INBOX_UPDATE_SUB_TEXT": "Posteingang Einstellungen aktualisieren",
"AUTO_ASSIGNMENT_SUB_TEXT": "Aktivieren oder deaktivieren Sie die automatische Zuweisung verfügbarer Agenten für neue Konversationen" "AUTO_ASSIGNMENT_SUB_TEXT": "Aktivieren oder deaktivieren Sie die automatische Zuweisung verfügbarer Agenten für neue Konversationen",
"HMAC_VERIFICATION": "User Identity Validation",
"HMAC_DESCRIPTION": "Inorder validate the users identity, the SDK allows you to pass an `identity_hash` for each user. You can generate HMAC using 'sha256' with the key shown here."
}, },
"FACEBOOK_REAUTHORIZE": { "FACEBOOK_REAUTHORIZE": {
"TITLE": "Neu autorisieren", "TITLE": "Neu autorisieren",
"SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services", "SUBTITLE": "Ihre Facebook-Verbindung ist abgelaufen, bitte verbinden Sie sich neu, um die Dienste fortzuführen",
"MESSAGE_SUCCESS": "Reconnection successful", "MESSAGE_SUCCESS": "Wiederverbindung erfolgreich",
"MESSAGE_ERROR": "There was an error, please try again" "MESSAGE_ERROR": "Es ist ein Fehler aufgetreten, bitte versuche es erneut"
} }
} }
} }

View File

@ -1,5 +1,5 @@
/* eslint-disable */
import { default as _agentMgmt } from './agentMgmt.json'; import { default as _agentMgmt } from './agentMgmt.json';
import { default as _labelsMgmt } from './labelsMgmt.json';
import { default as _cannedMgmt } from './cannedMgmt.json'; import { default as _cannedMgmt } from './cannedMgmt.json';
import { default as _chatlist } from './chatlist.json'; import { default as _chatlist } from './chatlist.json';
import { default as _contact } from './contact.json'; import { default as _contact } from './contact.json';
@ -23,6 +23,7 @@ export default {
..._inboxMgmt, ..._inboxMgmt,
..._login, ..._login,
..._report, ..._report,
..._labelsMgmt,
..._resetPassword, ..._resetPassword,
..._setNewPassword, ..._setNewPassword,
..._settings, ..._settings,

View File

@ -24,7 +24,7 @@
"FORM": { "FORM": {
"END_POINT": { "END_POINT": {
"LABEL": "Webhook-URL", "LABEL": "Webhook-URL",
"PLACEHOLDER": "Example: https://example/api/webhook", "PLACEHOLDER": "Beispiel: https://example/api/webhook",
"ERROR": "Bitte geben Sie eine gültige URL ein" "ERROR": "Bitte geben Sie eine gültige URL ein"
}, },
"SUBMIT": "Webhook erstellen" "SUBMIT": "Webhook erstellen"
@ -51,11 +51,11 @@
"DELETE": { "DELETE": {
"BUTTON_TEXT": "Löschen", "BUTTON_TEXT": "Löschen",
"API": { "API": {
"SUCCESS_MESSAGE": "Integration deleted successfully" "SUCCESS_MESSAGE": "Integration erfolgreich gelöscht"
} }
}, },
"CONNECT": { "CONNECT": {
"BUTTON_TEXT": "Connect" "BUTTON_TEXT": "Verbinden"
} }
} }
} }

View File

@ -2,8 +2,8 @@
"LOGIN": { "LOGIN": {
"TITLE": "Melden Sie sich bei Chatwoot an", "TITLE": "Melden Sie sich bei Chatwoot an",
"EMAIL": { "EMAIL": {
"LABEL": "Email", "LABEL": "E-Mail",
"PLACEHOLDER": "Email eg: someone@example.com" "PLACEHOLDER": "E-Mail z.B. jemand@example.com"
}, },
"PASSWORD": { "PASSWORD": {
"LABEL": "Passwort", "LABEL": "Passwort",

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