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
# 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
ENABLE_ACCOUNT_SIGNUP=true
ENABLE_ACCOUNT_SIGNUP=false
# Redis config
REDIS_URL=redis://redis:6379
@ -110,6 +110,12 @@ SLACK_CLIENT_SECRET=
## Mobile app env variables
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
## generate a new key value here : https://d3v.one/vapid-key-generator/
# VAPID_PUBLIC_KEY=

3
.gitignore vendored
View file

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

View file

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

View file

@ -6,13 +6,6 @@
# Note that changes in the inspected code, or installation of new
# 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
Lint/DuplicateMethods:
Exclude:

View file

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

View file

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

View file

@ -1,6 +1,6 @@
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
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
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>
*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",
"vue"
],
"success_url": "/app/login",
"success_url": "/",
"scripts": {
"postdeploy": "bundle exec rake db:seed"
},
"env": {
"SECRET_TOKEN": {
"description": "A secret key for verifying the integrity of signed cookies.",

View file

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

View file

@ -5,7 +5,7 @@ class ContactBuilder
contact_inbox = inbox.contact_inboxes.find_by(source_id: source_id)
return contact_inbox if contact_inbox
build_contact
build_contact_inbox
end
private
@ -26,16 +26,29 @@ class ContactBuilder
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
end
def build_contact
ActiveRecord::Base.transaction do
contact = account.contacts.create!(
def create_contact
account.contacts.create!(
name: contact_attributes[:name],
phone_number: contact_attributes[:phone_number],
email: contact_attributes[:email],
identifier: contact_attributes[:identifier],
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)
update_contact_avatar(contact)
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
def create
@conversation.update_labels(params[:labels])
@labels = @conversation.label_list
include LabelConcern
private
def model
@model ||= @conversation
end
# all labels of the current conversation
def index
@labels = @conversation.label_list
def permitted_params
params.permit(:conversation_id, labels: [])
end
end

View file

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

View file

@ -86,7 +86,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
account_id: Current.account.id,
inbox_id: @contact_inbox.inbox_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

View file

@ -3,10 +3,12 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
before_action :fetch_notification, only: [:update]
before_action :set_primary_actor, only: [:read_all]
before_action :set_current_page, only: [:index]
def index
@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
def read_all
@ -25,6 +27,11 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
render json: @notification
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
def set_primary_actor
@ -37,4 +44,12 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
def fetch_notification
@notification = current_user.notifications.find(params[:id])
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

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
@user, @account = AccountBuilder.new(
account_name: account_params[:account_name],
user_full_name: account_params[:user_full_name],
email: account_params[:email],
confirmed: confirmed?,
user: current_user
@ -29,6 +30,7 @@ class Api::V1::AccountsController < Api::BaseController
end
def show
@latest_chatwoot_version = ::Redis::Alfred.get(::Redis::Alfred::LATEST_CHATWOOT_VERSION)
render 'api/v1/accounts/show.json'
end
@ -54,7 +56,7 @@ class Api::V1::AccountsController < Api::BaseController
end
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
def check_signup_enabled

View file

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

View file

@ -7,8 +7,13 @@ class Api::V1::Widget::BaseController < ApplicationController
private
def conversations
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
def conversation
@conversation ||= conversations.last

View file

@ -1,5 +1,6 @@
class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
def update
process_hmac
contact_identify_action = ContactIdentifyAction.new(
contact: @contact,
params: permitted_params.to_h.deep_symbolize_keys
@ -9,7 +10,22 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
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
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

View file

@ -54,6 +54,8 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
end
def conversation_params
# FIXME: typo referrer in additional attributes
# will probably require a migration.
{
account_id: inbox.account_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')
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil
Current.reset
end
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
include SwitchLocale
before_action :set_global_config
around_action :switch_locale
before_action :ensure_installation_onboarding, only: [:index]
layout 'vueapp'
def index; end
@ -20,4 +25,8 @@ class DashboardController < ActionController::Base
APP_VERSION: Chatwoot.config[:version]
)
end
def ensure_installation_onboarding
redirect_to '/installation/onboarding' if ::Redis::Alfred.get(::Redis::Alfred::CHATWOOT_INSTALLATION_ONBOARDING)
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) => {
axios
.post(urlData.url, {
account_name: creds.name,
account_name: creds.accountName.trim(),
user_full_name: creds.fullName.trim(),
email: creds.email,
})
.then(response => {
@ -139,6 +140,12 @@ export default {
return axios.put(endPoints('profileUpdate').url, formData);
},
updateUISettings({ uiSettings }) {
return axios.put(endPoints('profileUpdate').url, {
profile: { ui_settings: uiSettings },
});
},
updateAvailability({ availability }) {
return axios.put(endPoints('profileUpdate').url, {
profile: { availability },

View file

@ -7,11 +7,26 @@ class MessageApi extends ApiClient {
super('conversations', { accountScoped: true });
}
create({ conversationId, message, private: isPrivate, contentAttributes }) {
return axios.post(`${this.url}/${conversationId}/messages`, {
content: message,
create({
conversationId,
message,
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 },
});
}
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();

View file

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

View file

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

View file

@ -23,7 +23,7 @@
@import 'views/settings/inbox';
@import 'views/settings/channel';
@import 'views/settings/integrations';
@import 'views/signup';
@import 'plugins/multiselect';
@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/font-size';
@import 'shared/assets/stylesheets/font-weights';
@import 'shared/assets/stylesheets/shadows';
@import 'shared/assets/stylesheets/border-radius';
@import 'variables';
@import '~spinkit/scss/spinners/7-three-bounce';
@import '~ionicons/scss/ionicons';
@import '~shared/assets/stylesheets/ionicons';
@import 'mixins';
@import 'foundation-settings';

View file

@ -1,15 +1,36 @@
@import '../variables';
.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 {
background-color: $alert-color;
background-color: var(--r-500);
border-radius: 5px;
color: $color-white;
color: var(--color-white);
font-size: 14px;
margin-bottom: 14px;
padding: 10px;
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 '../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 {
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 {
padding-left: $space-normal;
padding-right: $space-normal;

View file

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

View file

@ -3,7 +3,7 @@
@include margin($zero);
background: $color-woot;
border-radius: $space-one;
color: $color-white;
color: var(--white);
font-size: $font-size-small;
font-weight: $font-weight-normal;
position: relative;
@ -11,20 +11,12 @@
.message-text__wrap {
position: relative;
.link {
color: $color-white;
color: var(--white);
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 {
cursor: pointer;
position: relative;
@ -95,17 +87,15 @@
}
}
.content-box {
text-align: center;
}
}
.conversation-wrap {
@include background-gray;
@include margin(0);
@include border-normal-left;
background: var(--color-background-light);
.current-chat {
@include flex;
@ -145,10 +135,9 @@
@include flex-weight(1);
@include margin($zero);
flex-direction: column;
// Firefox flexbox fix
height: 100%;
margin-bottom: $space-small;
overflow-y: auto;
padding-bottom: var(--space-normal);
position: relative;
}
@ -171,7 +160,7 @@
@include elegant-card;
@include round-corner;
background: $color-woot;
color: $color-white;
color: var(--white);
font-size: $font-size-mini;
font-weight: $font-weight-medium;
margin: $space-one auto;
@ -201,6 +190,10 @@
color: $color-body;
margin-right: auto;
&.is-image {
border-radius: var(--border-radius-large);
}
.link {
color: $color-primary-dark;
}
@ -218,6 +211,7 @@
color: $color-primary-dark;
}
}
}
+.right {
@ -247,7 +241,6 @@
background: lighten($warning-color, 32%);
border: 1px solid lighten($warning-color, 15%);
color: $color-heading;
padding-right: $space-large;
position: relative;
&::before {
@ -258,6 +251,10 @@
top: $space-smaller + $space-micro;
}
}
&.is-image {
border-radius: var(--border-radius-large);
}
}
+.left {
@ -297,30 +294,15 @@
border-radius: $space-smaller;
font-size: $font-size-small;
p {
color: $color-heading;
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;
}
}
display: inline-block;
}
}
}
.activity-wrap .message-text__wrap {
.text-content p {
margin-bottom: 0;
}
}
@ -341,7 +323,7 @@
.typing-indicator {
@include elegant-card;
@include round-corner;
background: $color-white;
background: var(--white);
color: $color-light-gray;
font-size: $font-size-mini;
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},
select,
.multiselect > .multiselect__tags {
@include thin-border(darken(get-color(alert), 25%));
@include thin-border(var(--r-400));
}
.message {
color: darken(get-color(alert), 25%);
color: var(--r-400);
display: block;
font-size: var(--font-size-small);
font-weight: $font-weight-normal;
margin-bottom: $space-one;
margin-top: -$space-normal;

View file

@ -1,28 +1,13 @@
.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,
height 2s $swift-ease-out-function;
&.is-focused {
@include shadow;
box-shadow: var(--shadow);
}
.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 {
@include elegant-card;
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 {
color: $medium-gray;
cursor: pointer;
@ -65,9 +37,6 @@
}
}
.file-uploads>label {
cursor: pointer;
}
.attachment {
cursor: pointer;
@ -82,87 +51,45 @@
// Override min-height : 50px in foundation
//
max-height: $space-mega * 2.4;
min-height: 4rem;
min-height: 4.8rem;
padding: var(--space-normal) 0 0;
resize: none;
}
}
.reply-box__bottom {
@include background-light;
@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;
&.is-private {
background: var(--y-50);
.tabs {
border: 0;
flex: 1;
.reply-box__top {
background: var(--y-50);
>input {
background: var(--y-50);
}
}
}
.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);
.tabs-title {
margin: 0;
transition: all .2s $swift-ease-out-function;
transition-property: color, background;
a {
font-weight: $font-weight-medium;
padding: $space-one $space-two;
.file-uploads {
height: 100%;
line-height: var(--space-large);
width: 100%;
}
&.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;
}
label {
padding: var(--space-small);
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,12 @@
<template>
<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
v-if="showCannedResponsesList"
v-on-clickaway="hideCannedResponse"
@ -14,6 +20,7 @@
:on-click="emojiOnClick"
/>
<resizable-text-area
v-if="!isFormatMode"
ref="messageInput"
v-model="message"
class="input"
@ -24,71 +31,52 @@
@focus="onFocus"
@blur="onBlur"
/>
<file-upload
v-if="showFileUpload"
:size="4096 * 4096"
accept="image/*, application/pdf, audio/mpeg, video/mp4, audio/ogg, text/csv"
@input-file="onFileUpload"
>
<i v-if="!isUploading" class="icon ion-android-attach attachment" />
<woot-spinner v-if="isUploading" />
</file-upload>
<i
class="icon ion-happy-outline"
:class="{ active: showEmojiPicker }"
@click="toggleEmojiPicker"
<woot-message-editor
v-else
v-model="message"
class="input"
:placeholder="messagePlaceHolder"
:min-height="4"
@typing-off="onTypingOff"
@typing-on="onTypingOn"
@focus="onFocus"
@blur="onBlur"
/>
</div>
<div class="reply-box__bottom">
<ul class="tabs">
<li class="tabs-title" :class="{ 'is-active': !isPrivate }">
<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,
}"
<div v-if="hasAttachments" class="attachment-preview-box">
<attachment-preview
:attachments="attachedFiles"
:remove-attachment="removeAttachment"
/>
</button>
</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>
</template>
<script>
import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway';
import FileUpload from 'vue-upload-component';
import EmojiInput from 'shared/components/emoji/EmojiInput';
import CannedResponse from './CannedResponse';
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 {
isEscape,
isEnter,
@ -101,8 +89,11 @@ export default {
components: {
EmojiInput,
CannedResponse,
FileUpload,
ResizableTextArea,
AttachmentPreview,
ReplyTopPanel,
ReplyBottomPanel,
WootMessageEditor,
},
mixins: [clickaway, inboxMixin],
props: {
@ -114,18 +105,20 @@ export default {
data() {
return {
message: '',
isPrivateTabActive: false,
isFocused: false,
showEmojiPicker: false,
showCannedResponsesList: false,
attachedFiles: [],
isUploading: false,
replyType: REPLY_EDITOR_MODES.REPLY,
isFormatMode: false,
};
},
computed: {
...mapGetters({ currentChat: 'getSelectedChat' }),
isPrivate() {
if (this.currentChat.can_reply) {
return this.isPrivateTabActive;
return this.replyType === REPLY_EDITOR_MODES.NOTE;
}
return true;
},
@ -141,13 +134,15 @@ export default {
: this.$t('CONVERSATION.FOOTER.MSG_INPUT');
},
isMessageLengthReachingThreshold() {
return this.message.length > this.maxLength - 40;
return this.message.length > this.maxLength - 50;
},
characterCountIndicator() {
return `${this.message.length} / ${this.maxLength}`;
charactersRemaining() {
return this.maxLength - this.message.length;
},
isReplyButtonDisabled() {
const isMessageEmpty = !this.message.trim().replace(/\n/g, '').length;
if (this.hasAttachments) return false;
return (
isMessageEmpty ||
this.message.length === 0 ||
@ -196,16 +191,28 @@ export default {
},
replyBoxClass() {
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: {
currentChat(conversation) {
if (conversation.can_reply) {
this.isPrivateTabActive = false;
const { can_reply: canReply } = conversation;
if (canReply) {
this.replyType = REPLY_EDITOR_MODES.REPLY;
} else {
this.isPrivateTabActive = true;
this.replyType = REPLY_EDITOR_MODES.NOTE;
}
},
message(updatedMessage) {
@ -240,7 +247,9 @@ export default {
this.hideEmojiPicker();
this.hideCannedResponse();
} else if (isEnter(e)) {
if (!hasPressedShift(e)) {
const shouldSendMessage =
!this.isFormatMode && !hasPressedShift(e) && this.isFocused;
if (shouldSendMessage) {
e.preventDefault();
this.sendMessage();
}
@ -250,18 +259,11 @@ export default {
if (this.isReplyButtonDisabled) {
return;
}
const newMessage = this.message;
if (!this.showCannedResponsesList) {
const newMessage = this.message;
const messagePayload = this.getMessagePayload(newMessage);
this.clearMessage();
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);
this.$emit('scrollToMessage');
} catch (error) {
@ -275,12 +277,10 @@ export default {
this.message = message;
}, 100);
},
setPrivateReplyMode() {
this.isPrivateTabActive = true;
this.$refs.messageInput.focus();
},
setReplyMode() {
this.isPrivateTabActive = false;
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
const { can_reply: canReply } = this.currentChat;
if (canReply) this.replyType = mode;
this.$refs.messageInput.focus();
},
emojiOnClick(emoji) {
@ -288,6 +288,7 @@ export default {
},
clearMessage() {
this.message = '';
this.attachedFiles = [];
},
toggleEmojiPicker() {
this.showEmojiPicker = !this.showEmojiPicker;
@ -322,30 +323,91 @@ export default {
}
},
onFileUpload(file) {
this.attachedFiles = [];
if (!file) {
return;
}
this.isUploading = true;
this.$store
.dispatch('sendAttachment', [
this.currentChat.id,
{ file: file.file, isPrivate: this.isPrivate },
])
.then(() => {
this.isUploading = false;
this.$emit('scrollToMessage');
})
.catch(() => {
this.isUploading = false;
this.$emit('scrollToMessage');
const reader = new FileReader();
reader.readAsDataURL(file.file);
reader.onloadend = () => {
this.attachedFiles.push({
currentChatId: this.currentChat.id,
resource: file,
isPrivate: this.isPrivate,
thumb: reader.result,
});
};
},
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>
<style lang="scss">
<style lang="scss" scoped>
.send-button {
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>

View file

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

View file

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

View file

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

View file

@ -1,5 +1,8 @@
/* eslint no-console: 0 */
/* eslint no-param-reassign: 0 */
import getUuid from 'widget/helpers/uuid';
import { MESSAGE_STATUS, MESSAGE_TYPE } from 'shared/constants/messages';
export default () => {
if (!Array.prototype.last) {
Object.assign(Array.prototype, {
@ -26,3 +29,23 @@ export const getTypingUsersText = (users = []) => {
const rest = users.length - 1;
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', () => {
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');
});
});
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',
},
contacts: {
icon: 'ion-person-stalker',
icon: 'ion-person',
label: 'CONTACTS',
hasSubMenu: false,
toState: frontendURL(`accounts/${accountId}/contacts`),

View file

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

View file

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

View file

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

View file

@ -241,7 +241,9 @@
"AUTO_ASSIGNMENT": "تفعيل الإسناد التلقائي",
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
"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": {
"TITLE": "إعادة التصريح",

View file

@ -113,7 +113,7 @@
"SIDEBAR": {
"CONVERSATIONS": "المحادثات",
"REPORTS": "التقارير",
"CONTACTS": "Contacts (Beta)",
"CONTACTS": "Contacts",
"SETTINGS": "الإعدادات",
"HOME": "الرئيسية",
"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>",
"ACCOUNT_NAME": {
"LABEL": "اسم الحساب",
"PLACEHOLDER": "مؤسسة Wayne",
"ERROR": "اسم الحساب قصير جداً"
"PLACEHOLDER": "Enter an account name. eg: Wayne Enterprises",
"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": {
"LABEL": "البريد الإلكتروني",
"PLACEHOLDER": "bruce@wayne.enterprises",
"ERROR": "البريد الإلكتروني غير صالح"
"LABEL": "Work email",
"PLACEHOLDER": "Enter your work email address. eg: bruce@wayne.enterprises",
"ERROR": "Email address is invalid"
},
"PASSWORD": {
"LABEL": "كلمة المرور",
@ -28,12 +33,6 @@
"ERROR_MESSAGE": "تعذر الاتصال بالخادم، الرجاء المحاولة مرة أخرى لاحقاً"
},
"SUBMIT": "إرسال",
"FEATURES": {
"UNLIMITED_INBOXES": "Unlimited inboxes",
"ROBUST_REPORTING": "Robust Reporting",
"CANNED_RESPONSES": "الردود السريعة",
"AUTO_ASSIGNMENT": "Auto Assignment",
"SECURITY": "Enterprise level security"
}
"HAVE_AN_ACCOUNT": "Already have an account?"
}
}

View file

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

View file

@ -13,8 +13,8 @@
"PLACEHOLDER": "Escriu qualsevol text per cercar missatges",
"NO_MATCHING_RESULTS": "No hi ha missatges que coincideixin amb els paràmetres de cerca."
},
"UNREAD_MESSAGES": "Unread Messages",
"UNREAD_MESSAGE": "Unread Message",
"UNREAD_MESSAGES": "Missatges no Llegits",
"UNREAD_MESSAGE": "Missatge no Llegit",
"CLICK_HERE": "Clica aquí",
"LOADING_INBOXES": "S'estan carregant les safates d'entrada",
"LOADING_CONVERSATIONS": "S'estan carregant les converses",
@ -24,6 +24,7 @@
"REPLYING_TO": "Estas responent a:",
"REMOVE_SELECTION": "Elimina la selecció",
"DOWNLOAD": "Descarrega",
"UPLOADING_ATTACHMENTS": "Pujant fitxers adjunts...",
"HEADER": {
"RESOLVE_ACTION": "Resoldre",
"REOPEN_ACTION": "Tornar a obrir",
@ -40,7 +41,10 @@
"PRIVATE_NOTE": "Nota privada",
"SEND": "Envia",
"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",
"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.",
"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": {
"MULTISELECT": {

View file

@ -241,7 +241,9 @@
"AUTO_ASSIGNMENT": "Activa l'assignació automàtica",
"INBOX_UPDATE_TITLE": "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": {
"TITLE": "Reautoritza",

View file

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

View file

@ -3,7 +3,7 @@
"HEADER": "Informes",
"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.",
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
"DOWNLOAD_AGENT_REPORTS": "Descarregar Informes d'Agent",
"METRICS": {
"CONVERSATIONS": {
"NAME": "Converses",

View file

@ -113,7 +113,7 @@
"SIDEBAR": {
"CONVERSATIONS": "Converses",
"REPORTS": "Informes",
"CONTACTS": "Contactes (Beta)",
"CONTACTS": "Contactes",
"SETTINGS": "Configuracions",
"HOME": "Inici",
"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>",
"ACCOUNT_NAME": {
"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"
},
"EMAIL": {
"LABEL": "Correu electrònic",
"PLACEHOLDER": "bruce@wayne.enterprises",
"ERROR": "El correu electrònic no és correcte"
"LABEL": "Email de treball",
"PLACEHOLDER": "Introdueix la teva adreça email de treball. ex: bruce@wayne.enterprises",
"ERROR": "Adreça email invàlida"
},
"PASSWORD": {
"LABEL": "Contrasenya",
@ -28,12 +33,6 @@
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
},
"SUBMIT": "Envia",
"FEATURES": {
"UNLIMITED_INBOXES": "Safates ilimitades",
"ROBUST_REPORTING": "Informa robust",
"CANNED_RESPONSES": "Respostes predeterminades",
"AUTO_ASSIGNMENT": "Tasca Automàtica",
"SECURITY": "Seguretat a nivell empreserial"
}
"HAVE_AN_ACCOUNT": "Ja tens un compte?"
}
}

View file

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

View file

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

View file

@ -42,13 +42,14 @@
"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."
}
}
},
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
},
"FORMS": {
"MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select",
"ENTER_TO_REMOVE": "Press enter to remove",
"SELECT_ONE": "Select one"
"ENTER_TO_SELECT": "Stiskněte Enter pro vybrání",
"ENTER_TO_REMOVE": "Stiskněte Enter pro odebrání",
"SELECT_ONE": "Vyberte jeden"
}
}
}

View file

@ -75,10 +75,10 @@
},
"REPLY_TIME": {
"TITLE": "Nastavit čas odpovědi",
"IN_A_FEW_MINUTES": "In a few minutes",
"IN_A_FEW_HOURS": "In a few hours",
"IN_A_DAY": "In a day",
"HELP_TEXT": "This reply time will be displayed on the live chat widget"
"IN_A_FEW_MINUTES": "Do několika minut",
"IN_A_FEW_HOURS": "Do několika hodin",
"IN_A_DAY": "Do dne",
"HELP_TEXT": "Tento čas odpovědi bude zobrazen na widgetu"
},
"WIDGET_COLOR": {
"LABEL": "Barva widgetu",
@ -241,7 +241,9 @@
"AUTO_ASSIGNMENT": "Povolit automatické přiřazení",
"INBOX_UPDATE_TITLE": "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": {
"TITLE": "Znovu autorizovat",

View file

@ -113,7 +113,7 @@
"SIDEBAR": {
"CONVERSATIONS": "Konverzace",
"REPORTS": "Zprávy",
"CONTACTS": "Contacts (Beta)",
"CONTACTS": "Kontakty",
"SETTINGS": "Nastavení",
"HOME": "Home",
"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>",
"ACCOUNT_NAME": {
"LABEL": "Název účtu",
"PLACEHOLDER": "Wayne podniky",
"ERROR": "Název účtu je příliš krátký"
"PLACEHOLDER": "Enter an account name. eg: Wayne Enterprises",
"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": {
"LABEL": "E-mailová adresa",
"PLACEHOLDER": "bruce@wayne.enterprises",
"ERROR": "E-mail je neplatný"
"LABEL": "Work email",
"PLACEHOLDER": "Enter your work email address. eg: bruce@wayne.enterprises",
"ERROR": "Email address is invalid"
},
"PASSWORD": {
"LABEL": "Heslo",
@ -28,12 +33,6 @@
"ERROR_MESSAGE": "Nelze se připojit k Woot serveru, opakujte akci později"
},
"SUBMIT": "Odeslat",
"FEATURES": {
"UNLIMITED_INBOXES": "Unlimited inboxes",
"ROBUST_REPORTING": "Robust Reporting",
"CANNED_RESPONSES": "Konzervované odpovědi",
"AUTO_ASSIGNMENT": "Auto Assignment",
"SECURITY": "Enterprise level security"
}
"HAVE_AN_ACCOUNT": "Already have an account?"
}
}

View file

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

View file

@ -24,6 +24,7 @@
"REPLYING_TO": "Du svarer til:",
"REMOVE_SELECTION": "Fjern Markering",
"DOWNLOAD": "Download",
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
"HEADER": {
"RESOLVE_ACTION": "Løs",
"REOPEN_ACTION": "Genåben",
@ -40,7 +41,10 @@
"PRIVATE_NOTE": "Privat Note",
"SEND": "Send",
"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",
"CHANGE_STATUS": "Samtalestatus ændret",

View file

@ -42,7 +42,8 @@
"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."
}
}
},
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
},
"FORMS": {
"MULTISELECT": {

View file

@ -241,7 +241,9 @@
"AUTO_ASSIGNMENT": "Aktiver automatisk tildeling",
"INBOX_UPDATE_TITLE": "Indbakke Indstillinger",
"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": {
"TITLE": "Genautorisér",

View file

@ -113,7 +113,7 @@
"SIDEBAR": {
"CONVERSATIONS": "Samtaler",
"REPORTS": "Rapporter",
"CONTACTS": "Kontakter (Beta)",
"CONTACTS": "Kontakter",
"SETTINGS": "Indstillinger",
"HOME": "Hjem",
"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>",
"ACCOUNT_NAME": {
"LABEL": "Kontonavn",
"PLACEHOLDER": "Wayne Enterprises",
"ERROR": "Kontonavn er for kort"
"PLACEHOLDER": "Enter an account name. eg: Wayne Enterprises",
"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": {
"LABEL": "E-mail",
"PLACEHOLDER": "bruce@wayne.enterprises",
"ERROR": "E-mail er ugyldig"
"LABEL": "Work email",
"PLACEHOLDER": "Enter your work email address. eg: bruce@wayne.enterprises",
"ERROR": "Email address is invalid"
},
"PASSWORD": {
"LABEL": "Adgangskode",
@ -28,12 +33,6 @@
"ERROR_MESSAGE": "Kunne ikke oprette forbindelse til Woot Server, Prøv igen senere"
},
"SUBMIT": "Send",
"FEATURES": {
"UNLIMITED_INBOXES": "Unlimited inboxes",
"ROBUST_REPORTING": "Robust Reporting",
"CANNED_RESPONSES": "Standardsvar Svar",
"AUTO_ASSIGNMENT": "Auto Assignment",
"SECURITY": "Enterprise level security"
}
"HAVE_AN_ACCOUNT": "Already have an account?"
}
}

View file

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

View file

@ -1,113 +1,114 @@
{
"CONTACT_PANEL": {
"NOT_AVAILABLE": "Not Available",
"NOT_AVAILABLE": "Nicht verfügbar",
"EMAIL_ADDRESS": "E-Mail-Addresse",
"PHONE_NUMBER": "Telefonnummer",
"COPY_SUCCESSFUL": "Copied to clipboard successfully",
"COMPANY": "Company",
"COPY_SUCCESSFUL": "Der Code wurde erfolgreich in die Zwischenablage kopiert",
"COMPANY": "Firma",
"LOCATION": "Ort",
"CONVERSATION_TITLE": "Unterhaltungsdetails",
"BROWSER": "Browser",
"OS": "Betriebssystem",
"INITIATED_FROM": "Initiiert von",
"INITIATED_AT": "Initiiert bei",
"IP_ADDRESS": "IP-Adresse",
"CONVERSATIONS": {
"NO_RECORDS_FOUND": "Es sind keine vorherigen Gespräche mit diesem Kontakt verbunden.",
"TITLE": "Vorherige Gespräche"
},
"CUSTOM_ATTRIBUTES": {
"TITLE": "Custom Attributes"
"TITLE": "Benutzerdefinierte Attribute"
},
"LABELS": {
"TITLE": "Konversationsetiketten",
"MODAL": {
"TITLE": "Labels for",
"ACTIVE_LABELS": "Labels added to the conversation",
"INACTIVE_LABELS": "Labels available in the account",
"REMOVE": "Click on X icon to remove the label",
"ADD": "Click on + icon to add the label",
"UPDATE_BUTTON": "Update labels",
"TITLE": "Labels für",
"ACTIVE_LABELS": "Labels zur Unterhaltung hinzugefügt",
"INACTIVE_LABELS": "Verfügbare Labels im Konto",
"REMOVE": "Klicken Sie auf das X-Symbol, um das Label zu entfernen",
"ADD": "Klicken Sie auf das + Symbol, um ein Label hinzuzufügen",
"UPDATE_BUTTON": "Labels aktualisieren",
"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_AVAILABLE_LABELS": "There are no labels added to this conversation."
"NO_LABELS_TO_ADD": "Es sind keine weiteren Labels im Konto definiert.",
"NO_AVAILABLE_LABELS": "Zu dieser Unterhaltung wurden noch keine Labels hinzugefügt."
},
"MUTE_CONTACT": "Mute Conversation",
"UNMUTE_CONTACT": "Unmute Conversation",
"MUTED_SUCCESS": "This conversation is muted for 6 hours",
"UNMUTED_SUCCESS": "This conversation is unmuted",
"SEND_TRANSCRIPT": "Send Transcript",
"MUTE_CONTACT": "Unterhaltung stummschalten",
"UNMUTE_CONTACT": "Unterhaltung entmuten",
"MUTED_SUCCESS": "Diese Unterhaltung ist für 6 Stunden auf stumm schalten",
"UNMUTED_SUCCESS": "Diese Unterhaltung ist nicht mehr stumm geschaltet",
"SEND_TRANSCRIPT": "Transkript senden",
"EDIT_LABEL": "Bearbeiten"
},
"EDIT_CONTACT": {
"BUTTON_LABEL": "Edit Contact",
"TITLE": "Edit contact",
"DESC": "Edit contact details",
"BUTTON_LABEL": "Kontakt bearbeiten",
"TITLE": "Kontakt bearbeiten",
"DESC": "Kontaktdetails bearbeiten",
"FORM": {
"SUBMIT": "Einreichen",
"CANCEL": "Stornieren",
"AVATAR": {
"LABEL": "Contact Avatar"
"LABEL": "Kontaktbild"
},
"NAME": {
"PLACEHOLDER": "Enter the full name of the contact",
"LABEL": "Full Name"
"PLACEHOLDER": "Vollständigen Namen des Kontakts eingeben",
"LABEL": "Vollständiger Name"
},
"BIO": {
"PLACEHOLDER": "Enter the bio of the contact",
"LABEL": "Bio"
"PLACEHOLDER": "Beschreibung dieses Kontakts",
"LABEL": "Beschreibung"
},
"EMAIL_ADDRESS": {
"PLACEHOLDER": "Enter the email address of the contact",
"PLACEHOLDER": "Geben Sie die E-Mail-Adresse des Kontakts ein",
"LABEL": "E-Mail-Addresse"
},
"PHONE_NUMBER": {
"PLACEHOLDER": "Enter the phone number of the contact",
"LABEL": "Phone Number"
"PLACEHOLDER": "Geben Sie die Telefonnummer des Kontakts ein",
"LABEL": "Telefonnummer"
},
"LOCATION": {
"PLACEHOLDER": "Enter the location of the contact",
"PLACEHOLDER": "Geben Sie den Standort des Kontakts ein",
"LABEL": "Ort"
},
"COMPANY_NAME": {
"PLACEHOLDER": "Enter the company name",
"LABEL": "Company Name"
"PLACEHOLDER": "Firmenname eingeben",
"LABEL": "Firmenname"
},
"SOCIAL_PROFILES": {
"FACEBOOK": {
"PLACEHOLDER": "Enter the Facebook username",
"PLACEHOLDER": "Facebook-Benutzername eingeben",
"LABEL": "Facebook"
},
"TWITTER": {
"PLACEHOLDER": "Enter the Twitter username",
"PLACEHOLDER": "Twitter-Benutzername eingeben",
"LABEL": "Twitter"
},
"LINKEDIN": {
"PLACEHOLDER": "Enter the LinkedIn username",
"PLACEHOLDER": "Geben Sie den LinkedIn-Benutzernamen ein",
"LABEL": "LinkedIn"
},
"GITHUB": {
"PLACEHOLDER": "Enter the Github username",
"PLACEHOLDER": "Gitub-Benutzernamen eingeben",
"LABEL": "Github"
}
}
},
"SUCCESS_MESSAGE": "Updated contact successfully",
"CONTACT_ALREADY_EXIST": "This email address is in use for another contact.",
"ERROR_MESSAGE": "There was an error updating the contact, please try again"
"SUCCESS_MESSAGE": "Kontakt erfolgreich aktualisiert",
"CONTACT_ALREADY_EXIST": "Diese E-Mail-Adresse wird bereits für einen anderen Kontakt verwendet.",
"ERROR_MESSAGE": "Beim Aktualisieren des Kontakts ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut"
},
"CONTACTS_PAGE": {
"HEADER": "Contacts",
"SEARCH_BUTTON": "Search",
"SEARCH_INPUT_PLACEHOLDER": "Search for contacts",
"HEADER": "Kontakte",
"SEARCH_BUTTON": "Suchen",
"SEARCH_INPUT_PLACEHOLDER": "Suche nach Kontakten",
"LIST": {
"LOADING_MESSAGE": "Loading contacts...",
"404": "No contacts matches your search 🔍",
"LOADING_MESSAGE": "Kontakte werden geladen...",
"404": "Keine Kontakte entsprechend Deiner Suche gefunden 🔍",
"TABLE_HEADER": [
"Name",
"Phone Number",
"Telefonnummer",
"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_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",
"SEARCH_MESSAGES": "Search for messages in conversations",
"SEARCH_MESSAGES": "Nachrichten durchsuchen",
"SEARCH": {
"TITLE": "Search messages",
"LOADING_MESSAGE": "Crunching data...",
"PLACEHOLDER": "Type any text to search messages",
"NO_MATCHING_RESULTS": "There are no messages matching the search parameters."
"TITLE": "Nachrichten durchsuchen",
"LOADING_MESSAGE": "Daten werden geladen...",
"PLACEHOLDER": "Geben Sie einen Text ein, um danach zu suchen",
"NO_MATCHING_RESULTS": "Keine passenden Nachrichten gefunden."
},
"UNREAD_MESSAGES": "Unread Messages",
"UNREAD_MESSAGE": "Unread Message",
"UNREAD_MESSAGES": "Ungelesene Nachrichten",
"UNREAD_MESSAGE": "Ungelesene Nachricht",
"CLICK_HERE": "Hier klicken",
"LOADING_INBOXES": "Posteingänge laden",
"LOADING_CONVERSATIONS": "Gespräche laden",
"CANNOT_REPLY": "You cannot reply due to",
"24_HOURS_WINDOW": "24 hour message window restriction",
"LAST_INCOMING_TWEET": "You are replying to the last incoming tweet",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"CANNOT_REPLY": "Du kannst nicht Antworten aufgrund von",
"24_HOURS_WINDOW": "24-Stunden-Nachrichtenfenster-Beschränkung",
"LAST_INCOMING_TWEET": "Du antwortest auf den letzten eingehenden Tweet",
"REPLYING_TO": "Du antwortest auf:",
"REMOVE_SELECTION": "Auswahl entfernen",
"DOWNLOAD": "Herunterladen",
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
"HEADER": {
"RESOLVE_ACTION": "Fall schließen",
"REOPEN_ACTION": "Wieder öffnen",
@ -40,31 +41,34 @@
"PRIVATE_NOTE": "Private Notiz",
"SEND": "Senden",
"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",
"CHANGE_STATUS": "Gesprächsstatus geändert",
"CHANGE_AGENT": "Konversationsempfänger geändert",
"SENT_BY": "Sent by:",
"SENT_BY": "Gesendet von:",
"ASSIGNMENT": {
"SELECT_AGENT": "Select Agent",
"SELECT_AGENT": "Agent auswählen",
"REMOVE": "Entfernen",
"ASSIGN": "Assign"
"ASSIGN": "Zuordnen"
}
},
"EMAIL_TRANSCRIPT": {
"TITLE": "Send conversation transcript",
"DESC": "Send a copy of the conversation transcript to the specified email address",
"TITLE": "Konversations-Transkript senden",
"DESC": "Kopie des Konversationsprotokolls an die angegebene E-Mail-Adresse senden",
"SUBMIT": "Einreichen",
"CANCEL": "Stornieren",
"SEND_EMAIL_SUCCESS": "The chat transcript was sent successfully",
"SEND_EMAIL_ERROR": "There was an error, please try again",
"SEND_EMAIL_SUCCESS": "Das Chat-Protokoll wurde erfolgreich gesendet",
"SEND_EMAIL_ERROR": "Es ist ein Fehler aufgetreten, bitte versuche es erneut",
"FORM": {
"SEND_TO_CONTACT": "Send the transcript to the customer",
"SEND_TO_AGENT": "Send the transcript to the assigned agent",
"SEND_TO_OTHER_EMAIL_ADDRESS": "Send the transcript to another email address",
"SEND_TO_CONTACT": "Das Transkript an den Kunden senden",
"SEND_TO_AGENT": "Transkript an den zugewiesenen Agent senden",
"SEND_TO_OTHER_EMAIL_ADDRESS": "Transkript an eine andere E-Mail-Adresse senden",
"EMAIL": {
"PLACEHOLDER": "Enter an email address",
"PLACEHOLDER": "E-Mail-Adresse eingeben",
"ERROR": "Bitte geben Sie eine gültige E-Mail-Adresse ein"
}
}

View file

@ -2,7 +2,7 @@
"GENERAL_SETTINGS": {
"TITLE": "Kontoeinstellungen",
"SUBMIT": "Update Einstellungen",
"BACK": "Back",
"BACK": "Zurück",
"UPDATE": {
"ERROR": "Einstellungen konnten nicht aktualisiert werden, versuchen Sie es erneut!",
"SUCCESS": "Kontoeinstellungen erfolgreich aktualisiert"
@ -24,31 +24,32 @@
"ERROR": ""
},
"DOMAIN": {
"LABEL": "Incoming Email Domain",
"PLACEHOLDER": "The domain where you will receive the emails",
"LABEL": "Eingehende E-Mail-Domain",
"PLACEHOLDER": "Die Domain, von der E-Mails empfangen werden",
"ERROR": ""
},
"SUPPORT_EMAIL": {
"LABEL": "Support Email",
"PLACEHOLDER": "Your company's support email",
"LABEL": "Support-E-Mail",
"PLACEHOLDER": "Support E-Mail deines Unternehmens",
"ERROR": ""
},
"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",
"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": {
"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": {
"MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select",
"ENTER_TO_REMOVE": "Press enter to remove",
"SELECT_ONE": "Select one"
"ENTER_TO_SELECT": "Drücke Enter zum Auswählen",
"ENTER_TO_REMOVE": "Drücke Enter zum Entfernen",
"SELECT_ONE": "Eine Option wählen"
}
}
}

View file

@ -1,7 +1,7 @@
{
"INBOX_MGMT": {
"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": {
"404": "Diesem Konto sind keine Posteingänge zugeordnet."
},
@ -30,12 +30,12 @@
"ADD": {
"FB": {
"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_PLACEHOLDER": "Select a page from the list",
"INBOX_NAME": "Inbox Name",
"ADD_NAME": "Add a name for your inbox",
"PICK_NAME": "Pick A Name Your Inbox",
"PICK_A_VALUE": "Pick a value"
"CHOOSE_PAGE": "Seite auswählen",
"CHOOSE_PLACEHOLDER": "Wähle eine Seite aus der Liste",
"INBOX_NAME": "Name des Posteingang",
"ADD_NAME": "Namen für diesen Posteingang eingeben",
"PICK_NAME": "Wähle einen Namen für deinen Posteingang",
"PICK_A_VALUE": "Wähle einen Wert"
},
"TWITTER": {
"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": {
"TITLE": "Website-Kanal",
"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": {
"LABEL": "Channel Avatar"
},
@ -52,33 +52,33 @@
"PLACEHOLDER": "Geben Sie den Namen Ihrer Website ein (eg: Acme Inc)"
},
"CHANNEL_DOMAIN": {
"LABEL": "Website Domain",
"LABEL": "Website-Domain",
"PLACEHOLDER": "Geben Sie Ihre Website-Domain ein (eg: acme.com)"
},
"CHANNEL_WELCOME_TITLE": {
"LABEL": "Welcome Heading",
"PLACEHOLDER": "Hi there !"
"LABEL": "Willkommens-Überschrift",
"PLACEHOLDER": "Hallo!"
},
"CHANNEL_WELCOME_TAGLINE": {
"LABEL": "Welcome Tagline",
"PLACEHOLDER": "We make it simple to connect with us. Ask us anything, or share your feedback."
"LABEL": "Willkommens-Schlagzeile",
"PLACEHOLDER": "Wir machen es einfach, mit uns in Verbindung zu treten. Fragen Sie uns etwas oder teilen Sie Ihr Feedback."
},
"CHANNEL_GREETING_MESSAGE": {
"LABEL": "Channel greeting message",
"PLACEHOLDER": "Acme Inc typically replies in a few hours."
"LABEL": "Grußnachricht des Kanals",
"PLACEHOLDER": "Wir antworten in der Regel in wenigen Stunden."
},
"CHANNEL_GREETING_TOGGLE": {
"LABEL": "Enable channel greeting",
"HELP_TEXT": "Send a greeting message to the user when he starts the conversation.",
"LABEL": "Kanal Begrüßung aktivieren",
"HELP_TEXT": "Senden Sie eine Grußnachricht an den Benutzer, wenn er die Unterhaltung beginnt.",
"ENABLED": "Aktiviert",
"DISABLED": "Behindert"
},
"REPLY_TIME": {
"TITLE": "Reaktionszeit festlegen",
"IN_A_FEW_MINUTES": "In a few minutes",
"IN_A_FEW_HOURS": "In a few hours",
"IN_A_DAY": "In a day",
"HELP_TEXT": "This reply time will be displayed on the live chat widget"
"IN_A_FEW_MINUTES": "Innerhalb weniger Minuten",
"IN_A_FEW_HOURS": "Innerhalb weniger Stunden",
"IN_A_DAY": "Innerhalb eines Tages",
"HELP_TEXT": "Diese Antwortzeit wird im Live-Chat-Widget angezeigt"
},
"WIDGET_COLOR": {
"LABEL": "Widget Farbe",
@ -87,7 +87,7 @@
"SUBMIT_BUTTON": "Posteingang erstellen"
},
"TWILIO": {
"TITLE": "Twilio SMS/Whatsapp Channel",
"TITLE": "Twilio SMS/Whatsapp Kanal",
"DESC": "Integrieren Sie Twilio und unterstützen Sie Ihre Kunden per SMS/Whatsapp.",
"ACCOUNT_SID": {
"LABEL": "Account SID",
@ -95,11 +95,11 @@
"ERROR": "Dieses Feld wird benötigt"
},
"CHANNEL_TYPE": {
"LABEL": "Channel Type",
"ERROR": "Please select your Channel Type"
"LABEL": "Kanal-Typ",
"ERROR": "Bitte wählen Sie den Kanal-Typ aus"
},
"AUTH_TOKEN": {
"LABEL": "Auth Token",
"LABEL": "Auth-Token",
"PLACEHOLDER": "Bitte geben Sie Ihr Twilio Auth Token ein",
"ERROR": "Dieses Feld wird benötigt"
},
@ -115,7 +115,7 @@
},
"API_CALLBACK": {
"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",
"API": {
@ -123,8 +123,8 @@
}
},
"API_CHANNEL": {
"TITLE": "API Channel",
"DESC": "Integrate with API channel and start supporting your customers.",
"TITLE": "API-Kanal",
"DESC": "Integrieren Sie einen API-Kanal und starten Sie mit der Unterstützung Ihrer Kunden.",
"CHANNEL_NAME": {
"LABEL": "Kanal Name",
"PLACEHOLDER": "Bitte geben Sie einen Kanalnamen ein",
@ -132,32 +132,32 @@
},
"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"
},
"SUBMIT_BUTTON": "Create API Channel",
"SUBMIT_BUTTON": "API-Kanal erstellen",
"API": {
"ERROR_MESSAGE": "We were not able to save the api channel"
"ERROR_MESSAGE": "Der API-Kanal konnte nicht gespeichert werden"
}
},
"EMAIL_CHANNEL": {
"TITLE": "Email Channel",
"DESC": "Integrate you email inbox.",
"TITLE": "E-Mail-Kanal",
"DESC": "Integrieren Sie Ihren Posteingang.",
"CHANNEL_NAME": {
"LABEL": "Kanal Name",
"PLACEHOLDER": "Bitte geben Sie einen Kanalnamen ein",
"ERROR": "Dieses Feld wird benötigt"
},
"EMAIL": {
"LABEL": "Email",
"SUBTITLE": "Email where your customers sends you support tickets",
"PLACEHOLDER": "Email"
"LABEL": "E-Mail",
"SUBTITLE": "E-Mail Adresse, an die Ihre Kunden Ihnen Support-Tickets senden",
"PLACEHOLDER": "E-Mail"
},
"SUBMIT_BUTTON": "Create Email Channel",
"SUBMIT_BUTTON": "E-Mail-Kanal erstellen",
"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": {
"TITLE": "Kanäle",
@ -166,8 +166,8 @@
"AGENTS": {
"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.",
"VALIDATION_ERROR": "Add atleast one agent to your new Inbox",
"PICK_AGENTS": "Pick agents for the inbox"
"VALIDATION_ERROR": "Fügen Sie mindestens einen Agenten zu Ihrem neuen Posteingang hinzu",
"PICK_AGENTS": "Agenten für den Posteingang auswählen"
},
"DETAILS": {
"TITLE": "Posteingangsdetails",
@ -223,14 +223,14 @@
},
"TABS": {
"SETTINGS": "die Einstellungen",
"COLLABORATORS": "Collaborators",
"CONFIGURATION": "Configuration"
"COLLABORATORS": "Mitarbeitende",
"CONFIGURATION": "Konfiguration"
},
"SETTINGS": "die Einstellungen",
"FEATURES": {
"LABEL": "Features",
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget"
"LABEL": "Funktionen",
"DISPLAY_FILE_PICKER": "Dateiauswahl im Widget anzeigen",
"DISPLAY_EMOJI_PICKER": "Emoji-Auswahl im Widget anzeigen"
},
"SETTINGS_POPUP": {
"MESSENGER_HEADING": "Messenger-Skript",
@ -239,15 +239,17 @@
"INBOX_AGENTS_SUB_TEXT": "Hinzufügen oder Entfernen von Agenten zu diesem Posteingang",
"UPDATE": "Aktualisieren",
"AUTO_ASSIGNMENT": "Aktivieren Sie die automatische Zuweisung",
"INBOX_UPDATE_TITLE": "Inbox Settings",
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
"AUTO_ASSIGNMENT_SUB_TEXT": "Aktivieren oder deaktivieren Sie die automatische Zuweisung verfügbarer Agenten für neue Konversationen"
"INBOX_UPDATE_TITLE": "Posteingang Einstellungen",
"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",
"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": {
"TITLE": "Neu autorisieren",
"SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"MESSAGE_SUCCESS": "Reconnection successful",
"MESSAGE_ERROR": "There was an error, please try again"
"SUBTITLE": "Ihre Facebook-Verbindung ist abgelaufen, bitte verbinden Sie sich neu, um die Dienste fortzuführen",
"MESSAGE_SUCCESS": "Wiederverbindung erfolgreich",
"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 _labelsMgmt } from './labelsMgmt.json';
import { default as _cannedMgmt } from './cannedMgmt.json';
import { default as _chatlist } from './chatlist.json';
import { default as _contact } from './contact.json';
@ -23,6 +23,7 @@ export default {
..._inboxMgmt,
..._login,
..._report,
..._labelsMgmt,
..._resetPassword,
..._setNewPassword,
..._settings,

View file

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

View file

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

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