Merge branch 'release/1.12.0' into master
This commit is contained in:
commit
1aaced3027
573 changed files with 6836 additions and 2471 deletions
|
@ -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
3
.gitignore
vendored
|
@ -60,3 +60,6 @@ package-lock.json
|
|||
|
||||
# cypress
|
||||
test/cypress/videos/*
|
||||
|
||||
/config/master.key
|
||||
/config/*.enc
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
7
Gemfile
7
Gemfile
|
@ -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'
|
||||
|
|
53
Gemfile.lock
53
Gemfile.lock
|
@ -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
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
|
@ -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* © 2017-2020, ThoughtWoot Inc - Released under the MIT License.
|
||||
*Chatwoot* © 2017-2021, Chatwoot Inc - Released under the MIT License.
|
||||
|
|
5
app.json
5
app.json
|
@ -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.",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!(
|
||||
name: contact_attributes[:name],
|
||||
phone_number: contact_attributes[:phone_number],
|
||||
email: contact_attributes[:email],
|
||||
identifier: contact_attributes[:identifier],
|
||||
additional_attributes: contact_attributes[:additional_attributes]
|
||||
)
|
||||
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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
24
app/controllers/api/v1/accounts/team_members_controller.rb
Normal file
24
app/controllers/api/v1/accounts/team_members_controller.rb
Normal 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
|
34
app/controllers/api/v1/accounts/teams_controller.rb
Normal file
34
app/controllers/api/v1/accounts/teams_controller.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -23,7 +23,8 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||
:password,
|
||||
:password_confirmation,
|
||||
:avatar,
|
||||
:availability
|
||||
:availability,
|
||||
ui_settings: {}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,12 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
private
|
||||
|
||||
def conversations
|
||||
@conversations = @contact_inbox.conversations.where(inbox_id: auth_token_params[:inbox_id])
|
||||
if @contact_inbox.hmac_verified?
|
||||
verified_contact_inbox_ids = @contact.contact_inboxes.where(inbox_id: auth_token_params[:inbox_id], hmac_verified: true).map(&:id)
|
||||
@conversations = @contact.conversations.where(contact_inbox_id: verified_contact_inbox_ids)
|
||||
else
|
||||
@conversations = @contact_inbox.conversations.where(inbox_id: auth_token_params[:inbox_id])
|
||||
end
|
||||
end
|
||||
|
||||
def conversation
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
10
app/controllers/concerns/label_concern.rb
Normal file
10
app/controllers/concerns/label_concern.rb
Normal 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
|
|
@ -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
|
||||
|
|
36
app/controllers/installation/onboarding_controller.rb
Normal file
36
app/controllers/installation/onboarding_controller.rb
Normal 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
|
29
app/controllers/platform/api/v1/account_users_controller.rb
Normal file
29
app/controllers/platform/api/v1/account_users_controller.rb
Normal 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
|
32
app/controllers/platform/api/v1/accounts_controller.rb
Normal file
32
app/controllers/platform/api/v1/accounts_controller.rb
Normal 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
|
43
app/controllers/platform/api/v1/users_controller.rb
Normal file
43
app/controllers/platform/api/v1/users_controller.rb
Normal 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
|
37
app/controllers/platform_controller.rb
Normal file
37
app/controllers/platform_controller.rb
Normal 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
|
|
@ -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
|
66
app/dashboards/installation_config_dashboard.rb
Normal file
66
app/dashboards/installation_config_dashboard.rb
Normal 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
|
15
app/fields/serialized_field.rb
Normal file
15
app/fields/serialized_field.rb
Normal 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
|
|
@ -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 },
|
||||
|
|
|
@ -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,
|
||||
private: isPrivate,
|
||||
content_attributes: contentAttributes,
|
||||
create({
|
||||
conversationId,
|
||||
message,
|
||||
private: isPrivate,
|
||||
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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
@import 'shared/assets/fonts/inter';
|
||||
@import '../variables';
|
||||
@import '~ionicons/scss/ionicons';
|
||||
@import '~shared/assets/stylesheets/ionicons';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
.message-text__wrap {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
padding: 0;
|
||||
.reply-box__top {
|
||||
background: var(--y-50);
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
&.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;
|
||||
>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);
|
||||
|
||||
.file-uploads {
|
||||
height: 100%;
|
||||
line-height: var(--space-large);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
label {
|
||||
padding: var(--space-small);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,5 +42,9 @@ export default {
|
|||
font-weight: $font-weight-medium;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.title--section {
|
||||
padding-right: var(--space-large);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="button nice resolve--button"
|
||||
class="button resolve--button"
|
||||
:class="buttonClass"
|
||||
@click="toggleStatus"
|
||||
>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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',
|
||||
};
|
|
@ -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: {
|
||||
|
|
|
@ -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 {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
.wrap {
|
||||
> .bubble {
|
||||
&.is-image {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
.image {
|
||||
max-width: 32rem;
|
||||
padding: 0;
|
||||
.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 {
|
||||
|
|
|
@ -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,
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
<div v-if="hasAttachments" class="attachment-preview-box">
|
||||
<attachment-preview
|
||||
:attachments="attachedFiles"
|
||||
:remove-attachment="removeAttachment"
|
||||
/>
|
||||
</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>
|
||||
|
|
|
@ -19,9 +19,13 @@
|
|||
class="icon ion-reply cursor-pointer"
|
||||
@click="onTweetReply"
|
||||
/>
|
||||
<a :href="linkToTweet" target="_blank" rel="noopener noreferrer nofollow">
|
||||
<a
|
||||
v-if="isATweet && isIncoming"
|
||||
:href="linkToTweet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
>
|
||||
<i
|
||||
v-if="isATweet && isIncoming"
|
||||
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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
11
app/javascript/dashboard/helper/files.js
Normal file
11
app/javascript/dashboard/helper/files.js
Normal 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];
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
18
app/javascript/dashboard/helper/specs/files.spec.js
Normal file
18
app/javascript/dashboard/helper/specs/files.spec.js
Normal 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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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`),
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"OS": "نظام التشغيل",
|
||||
"INITIATED_FROM": "تم البدء من",
|
||||
"INITIATED_AT": "تم البدء في",
|
||||
"IP_ADDRESS": "IP Address",
|
||||
"CONVERSATIONS": {
|
||||
"NO_RECORDS_FOUND": "لا توجد محادثات سابقة مرتبطة بجهة الاتصال هذه.",
|
||||
"TITLE": "المحادثات السابقة"
|
||||
|
|
|
@ -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": "تم تغيير حالة المحادثة",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": "إعادة التصريح",
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
"SIDEBAR": {
|
||||
"CONVERSATIONS": "المحادثات",
|
||||
"REPORTS": "التقارير",
|
||||
"CONTACTS": "Contacts (Beta)",
|
||||
"CONTACTS": "Contacts",
|
||||
"SETTINGS": "الإعدادات",
|
||||
"HOME": "الرئيسية",
|
||||
"AGENTS": "موظف الدعم",
|
||||
|
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
"SIDEBAR": {
|
||||
"CONVERSATIONS": "Converses",
|
||||
"REPORTS": "Informes",
|
||||
"CONTACTS": "Contactes (Beta)",
|
||||
"CONTACTS": "Contactes",
|
||||
"SETTINGS": "Configuracions",
|
||||
"HOME": "Inici",
|
||||
"AGENTS": "Agents",
|
||||
|
|
|
@ -5,13 +5,18 @@
|
|||
"TERMS_ACCEPT": "En registrar-vos, esteu d’acord 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?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
"SIDEBAR": {
|
||||
"CONVERSATIONS": "Konverzace",
|
||||
"REPORTS": "Zprávy",
|
||||
"CONTACTS": "Contacts (Beta)",
|
||||
"CONTACTS": "Kontakty",
|
||||
"SETTINGS": "Nastavení",
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Agenti",
|
||||
|
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -113,7 +113,7 @@
|
|||
"SIDEBAR": {
|
||||
"CONVERSATIONS": "Samtaler",
|
||||
"REPORTS": "Rapporter",
|
||||
"CONTACTS": "Kontakter (Beta)",
|
||||
"CONTACTS": "Kontakter",
|
||||
"SETTINGS": "Indstillinger",
|
||||
"HOME": "Hjem",
|
||||
"AGENTS": "Agenter",
|
||||
|
|
|
@ -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?"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue