Merge branch 'release/1.13.0'
This commit is contained in:
commit
ec1ddc06fd
6
Gemfile
6
Gemfile
@ -27,10 +27,10 @@ gem 'uglifier'
|
||||
gem 'flag_shih_tzu'
|
||||
# Random name generator for user names
|
||||
gem 'haikunator'
|
||||
# Template parsing safetly
|
||||
# Template parsing safely
|
||||
gem 'liquid'
|
||||
# Parse Markdown to HTML
|
||||
gem 'redcarpet'
|
||||
gem 'commonmarker'
|
||||
|
||||
##-- for active storage --##
|
||||
gem 'aws-sdk-s3', require: false
|
||||
@ -44,6 +44,8 @@ gem 'pg'
|
||||
gem 'redis'
|
||||
gem 'redis-namespace'
|
||||
gem 'redis-rack-cache'
|
||||
# super fast record imports in bulk
|
||||
gem 'activerecord-import'
|
||||
|
||||
##--- gems for server & infra configuration ---##
|
||||
gem 'dotenv-rails'
|
||||
|
||||
132
Gemfile.lock
132
Gemfile.lock
@ -18,56 +18,58 @@ GEM
|
||||
specs:
|
||||
action-cable-testing (0.6.1)
|
||||
actioncable (>= 5.0)
|
||||
actioncable (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
actioncable (6.0.3.5)
|
||||
actionpack (= 6.0.3.5)
|
||||
nio4r (~> 2.0)
|
||||
websocket-driver (>= 0.6.1)
|
||||
actionmailbox (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
actionmailbox (6.0.3.5)
|
||||
actionpack (= 6.0.3.5)
|
||||
activejob (= 6.0.3.5)
|
||||
activerecord (= 6.0.3.5)
|
||||
activestorage (= 6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
mail (>= 2.7.1)
|
||||
actionmailer (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
actionmailer (6.0.3.5)
|
||||
actionpack (= 6.0.3.5)
|
||||
actionview (= 6.0.3.5)
|
||||
activejob (= 6.0.3.5)
|
||||
mail (~> 2.5, >= 2.5.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
actionpack (6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
actionpack (6.0.3.5)
|
||||
actionview (= 6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
rack (~> 2.0, >= 2.0.8)
|
||||
rack-test (>= 0.6.3)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.0, >= 1.2.0)
|
||||
actiontext (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
actiontext (6.0.3.5)
|
||||
actionpack (= 6.0.3.5)
|
||||
activerecord (= 6.0.3.5)
|
||||
activestorage (= 6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
nokogiri (>= 1.8.5)
|
||||
actionview (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
actionview (6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
builder (~> 3.1)
|
||||
erubi (~> 1.4)
|
||||
rails-dom-testing (~> 2.0)
|
||||
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
||||
activejob (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
activejob (6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
globalid (>= 0.3.6)
|
||||
activemodel (6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
activerecord (6.0.3.4)
|
||||
activemodel (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
activestorage (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activemodel (6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
activerecord (6.0.3.5)
|
||||
activemodel (= 6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
activerecord-import (1.0.7)
|
||||
activerecord (>= 3.2)
|
||||
activestorage (6.0.3.5)
|
||||
actionpack (= 6.0.3.5)
|
||||
activejob (= 6.0.3.5)
|
||||
activerecord (= 6.0.3.5)
|
||||
marcel (~> 0.3.1)
|
||||
activesupport (6.0.3.4)
|
||||
activesupport (6.0.3.5)
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
i18n (>= 0.7, < 2)
|
||||
minitest (~> 5.1)
|
||||
@ -145,7 +147,9 @@ GEM
|
||||
coderay (1.1.3)
|
||||
coercible (1.0.0)
|
||||
descendants_tracker (~> 0.0.1)
|
||||
concurrent-ruby (1.1.7)
|
||||
commonmarker (0.21.1)
|
||||
ruby-enum (~> 0.5)
|
||||
concurrent-ruby (1.1.8)
|
||||
connection_pool (2.2.3)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
@ -181,7 +185,7 @@ GEM
|
||||
dotenv (= 2.7.6)
|
||||
railties (>= 3.2)
|
||||
equalizer (0.0.11)
|
||||
erubi (1.9.0)
|
||||
erubi (1.10.0)
|
||||
et-orbi (1.2.4)
|
||||
tzinfo
|
||||
execjs (2.7.0)
|
||||
@ -257,7 +261,7 @@ GEM
|
||||
mime-types (~> 3.0)
|
||||
multi_xml (>= 0.5.2)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.8.5)
|
||||
i18n (1.8.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ice_nine (0.11.2)
|
||||
inflecto (0.0.2)
|
||||
@ -294,7 +298,7 @@ GEM
|
||||
listen (3.3.3)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.7.0)
|
||||
loofah (2.9.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@ -311,7 +315,7 @@ GEM
|
||||
mini_magick (4.10.1)
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.5.0)
|
||||
minitest (5.14.2)
|
||||
minitest (5.14.3)
|
||||
momentjs-rails (2.20.1)
|
||||
railties (>= 3.1)
|
||||
msgpack (1.3.3)
|
||||
@ -321,8 +325,8 @@ GEM
|
||||
net-http-persistent (4.0.0)
|
||||
connection_pool (~> 2.2)
|
||||
netrc (0.11.0)
|
||||
nio4r (2.5.4)
|
||||
nokogiri (1.11.0)
|
||||
nio4r (2.5.5)
|
||||
nokogiri (1.11.1)
|
||||
mini_portile2 (~> 2.5.0)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.4)
|
||||
@ -353,38 +357,37 @@ GEM
|
||||
rack
|
||||
rack-test (1.1.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails (6.0.3.4)
|
||||
actioncable (= 6.0.3.4)
|
||||
actionmailbox (= 6.0.3.4)
|
||||
actionmailer (= 6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
actiontext (= 6.0.3.4)
|
||||
actionview (= 6.0.3.4)
|
||||
activejob (= 6.0.3.4)
|
||||
activemodel (= 6.0.3.4)
|
||||
activerecord (= 6.0.3.4)
|
||||
activestorage (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
rails (6.0.3.5)
|
||||
actioncable (= 6.0.3.5)
|
||||
actionmailbox (= 6.0.3.5)
|
||||
actionmailer (= 6.0.3.5)
|
||||
actionpack (= 6.0.3.5)
|
||||
actiontext (= 6.0.3.5)
|
||||
actionview (= 6.0.3.5)
|
||||
activejob (= 6.0.3.5)
|
||||
activemodel (= 6.0.3.5)
|
||||
activerecord (= 6.0.3.5)
|
||||
activestorage (= 6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
bundler (>= 1.3.0)
|
||||
railties (= 6.0.3.4)
|
||||
railties (= 6.0.3.5)
|
||||
sprockets-rails (>= 2.0.0)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.3.0)
|
||||
loofah (~> 2.3)
|
||||
railties (6.0.3.4)
|
||||
actionpack (= 6.0.3.4)
|
||||
activesupport (= 6.0.3.4)
|
||||
railties (6.0.3.5)
|
||||
actionpack (= 6.0.3.5)
|
||||
activesupport (= 6.0.3.5)
|
||||
method_source
|
||||
rake (>= 0.8.7)
|
||||
thor (>= 0.20.3, < 2.0)
|
||||
rainbow (3.0.0)
|
||||
rake (13.0.1)
|
||||
rake (13.0.3)
|
||||
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)
|
||||
@ -444,6 +447,8 @@ GEM
|
||||
rubocop (>= 0.87.0)
|
||||
rubocop-rspec (1.43.2)
|
||||
rubocop (~> 0.87)
|
||||
ruby-enum (0.9.0)
|
||||
i18n
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby2ruby (2.4.4)
|
||||
ruby_parser (~> 3.1)
|
||||
@ -520,7 +525,7 @@ GEM
|
||||
inflecto
|
||||
virtus
|
||||
telephone_number (1.4.9)
|
||||
thor (1.0.1)
|
||||
thor (1.1.0)
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.0.10)
|
||||
time_diff (0.3.0)
|
||||
@ -532,7 +537,7 @@ GEM
|
||||
nokogiri (>= 1.6, < 2.0)
|
||||
twitty (0.1.1)
|
||||
oauth
|
||||
tzinfo (1.2.8)
|
||||
tzinfo (1.2.9)
|
||||
thread_safe (~> 0.1)
|
||||
tzinfo-data (1.2020.1)
|
||||
tzinfo (>= 1.0.0)
|
||||
@ -575,13 +580,14 @@ GEM
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
wisper (2.0.0)
|
||||
zeitwerk (2.4.1)
|
||||
zeitwerk (2.4.2)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
action-cable-testing
|
||||
activerecord-import
|
||||
acts-as-taggable-on
|
||||
administrate
|
||||
annotate
|
||||
@ -595,6 +601,7 @@ DEPENDENCIES
|
||||
bullet
|
||||
bundle-audit
|
||||
byebug
|
||||
commonmarker
|
||||
cypress-on-rails (~> 1.0)
|
||||
database_cleaner
|
||||
devise
|
||||
@ -629,7 +636,6 @@ DEPENDENCIES
|
||||
pundit
|
||||
rack-cors
|
||||
rails
|
||||
redcarpet
|
||||
redis
|
||||
redis-namespace
|
||||
redis-rack-cache
|
||||
|
||||
@ -1,21 +1,14 @@
|
||||
class Api::V1::Accounts::Contacts::ContactInboxesController < Api::V1::Accounts::BaseController
|
||||
before_action :ensure_contact
|
||||
before_action :ensure_inbox, only: [:create]
|
||||
before_action :validate_channel_type
|
||||
|
||||
def create
|
||||
source_id = params[:source_id] || SecureRandom.uuid
|
||||
@contact_inbox = ContactInbox.create(contact: @contact, inbox: @inbox, source_id: source_id)
|
||||
@contact_inbox = ContactInbox.create!(contact: @contact, inbox: @inbox, source_id: source_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_channel_type
|
||||
return if @inbox.channel_type == 'Channel::Api'
|
||||
|
||||
render json: { error: 'Contact Inbox creation is only allowed in API inboxes' }, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def ensure_inbox
|
||||
@inbox = Current.account.inboxes.find(params[:inbox_id])
|
||||
end
|
||||
|
||||
@ -22,6 +22,14 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
@contacts = fetch_contact_last_seen_at(contacts)
|
||||
end
|
||||
|
||||
def import
|
||||
ActiveRecord::Base.transaction do
|
||||
import = Current.account.data_imports.create!(data_type: 'contacts')
|
||||
import.import_file.attach(params[:import_file])
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
# returns online contacts
|
||||
def active
|
||||
contacts = Current.account.contacts.where(id: ::OnlineStatusTracker
|
||||
@ -46,7 +54,7 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController
|
||||
rescue ActiveRecord::RecordInvalid => e
|
||||
render json: {
|
||||
message: e.record.errors.full_messages.join(', '),
|
||||
contact: Contact.find_by(email: contact_params[:email])
|
||||
contact: Current.account.contacts.find_by(email: contact_params[:email])
|
||||
}, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
|
||||
@ -1,9 +1,20 @@
|
||||
class Api::V1::Accounts::Conversations::AssignmentsController < Api::V1::Accounts::Conversations::BaseController
|
||||
# assign agent to a conversation
|
||||
# assigns agent/team to a conversation
|
||||
def create
|
||||
set_assignee
|
||||
render json: @assignee
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_assignee
|
||||
# if params[:assignee_id] is not a valid id, it will set to nil, hence unassigning the conversation
|
||||
assignee = Current.account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(assignee)
|
||||
render json: assignee
|
||||
if params.key?(:assignee_id)
|
||||
@assignee = Current.account.users.find_by(id: params[:assignee_id])
|
||||
@conversation.update_assignee(@assignee)
|
||||
elsif params.key?(:team_id)
|
||||
@assignee = Current.account.teams.find_by(id: params[:team_id])
|
||||
@conversation.update!(team: @assignee)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -22,9 +22,10 @@ class Api::V1::Accounts::InboxMembersController < Api::V1::Accounts::BaseControl
|
||||
# get the list of user_ids from params
|
||||
# the missing ones are the agents which are to be deleted from the inbox
|
||||
# the new ones are the agents which are to be added to the inbox
|
||||
|
||||
agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
|
||||
agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
|
||||
ActiveRecord::Base.transaction do
|
||||
agents_to_be_added_ids.each { |user_id| @inbox.add_member(user_id) }
|
||||
agents_to_be_removed_ids.each { |user_id| @inbox.remove_member(user_id) }
|
||||
end
|
||||
end
|
||||
|
||||
def agents_to_be_added_ids
|
||||
|
||||
@ -89,6 +89,8 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
|
||||
:webhook_url,
|
||||
:email,
|
||||
:reply_time,
|
||||
:pre_chat_form_enabled,
|
||||
{ pre_chat_form_options: [:pre_chat_message, :require_email] },
|
||||
{ selected_feature_flags: [] }
|
||||
])
|
||||
end
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseController
|
||||
protect_from_forgery with: :null_session
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
protect_from_forgery with: :null_session
|
||||
before_action :fetch_notification, only: [:update]
|
||||
before_action :set_primary_actor, only: [:read_all]
|
||||
before_action :set_current_page, only: [:index]
|
||||
@ -8,17 +9,18 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
def index
|
||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
||||
@count = notifications.count
|
||||
@notifications = notifications.page @current_page
|
||||
@notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
end
|
||||
|
||||
def read_all
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
if @primary_actor
|
||||
current_user.notifications.where(account_id: current_account.id, primary_actor: @primary_actor, read_at: nil)
|
||||
.update(read_at: DateTime.now.utc)
|
||||
.update_all(read_at: DateTime.now.utc)
|
||||
else
|
||||
current_user.notifications.where(account_id: current_account.id, read_at: nil).update(read_at: DateTime.now.utc)
|
||||
current_user.notifications.where(account_id: current_account.id, read_at: nil).update_all(read_at: DateTime.now.utc)
|
||||
end
|
||||
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@ -7,17 +7,41 @@ class Api::V1::Accounts::TeamMembersController < Api::V1::Accounts::BaseControll
|
||||
end
|
||||
|
||||
def create
|
||||
record = @team.team_members.find_or_create_by(user_id: params[:user_id])
|
||||
@team_member = record.user
|
||||
ActiveRecord::Base.transaction do
|
||||
@team_members = params[:user_ids].map { |user_id| @team.add_member(user_id) }
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
members_to_be_added_ids.each { |user_id| @team.add_member(user_id) }
|
||||
members_to_be_removed_ids.each { |user_id| @team.remove_member(user_id) }
|
||||
end
|
||||
@team_members = @team.members
|
||||
render action: 'create'
|
||||
end
|
||||
|
||||
def destroy
|
||||
@team.team_members.find_by(user_id: params[:user_id])&.destroy
|
||||
ActiveRecord::Base.transaction do
|
||||
params[:user_ids].map { |user_id| @team.remove_member(user_id) }
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def members_to_be_added_ids
|
||||
params[:user_ids] - current_members_ids
|
||||
end
|
||||
|
||||
def members_to_be_removed_ids
|
||||
current_members_ids - params[:user_ids]
|
||||
end
|
||||
|
||||
def current_members_ids
|
||||
@current_members_ids ||= @team.members.pluck(:id)
|
||||
end
|
||||
|
||||
def fetch_team
|
||||
@team = Current.account.teams.find(params[:team_id])
|
||||
end
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
class Api::V1::Accounts::Twitter::AuthorizationsController < Api::V1::Accounts::BaseController
|
||||
include TwitterConcern
|
||||
|
||||
before_action :check_authorization
|
||||
|
||||
def create
|
||||
@response = twitter_client.request_oauth_token(url: twitter_callback_url)
|
||||
if @response.status == '200'
|
||||
::Redis::Alfred.setex(oauth_token, Current.account.id)
|
||||
render json: { success: true, url: oauth_authorize_endpoint(oauth_token) }
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def oauth_token
|
||||
parsed_body['oauth_token']
|
||||
end
|
||||
|
||||
def oauth_authorize_endpoint(oauth_token)
|
||||
"#{twitter_api_base_url}/oauth/authorize?oauth_token=#{oauth_token}"
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
raise Pundit::NotAuthorizedError unless Current.account_user.administrator?
|
||||
end
|
||||
end
|
||||
@ -20,11 +20,7 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
end
|
||||
|
||||
def auth_token_params
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers[header_name]).decode_token
|
||||
end
|
||||
|
||||
def header_name
|
||||
'X-Auth-Token'
|
||||
@auth_token_params ||= ::Widget::TokenService.new(token: request.headers['X-Auth-Token']).decode_token
|
||||
end
|
||||
|
||||
def set_web_widget
|
||||
@ -39,6 +35,50 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
@contact = @contact_inbox.contact
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
::Conversation.create!(conversation_params)
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= ::Inbox.find_by(id: auth_token_params[:inbox_id])
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
# FIXME: typo referrer in additional attributes, will probably require a migration.
|
||||
{
|
||||
account_id: inbox.account_id,
|
||||
inbox_id: inbox.id,
|
||||
contact_id: @contact.id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: {
|
||||
browser: browser_params,
|
||||
referer: permitted_params[:message][:referer_url],
|
||||
initiated_at: timestamp_params
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def update_contact(email)
|
||||
contact_with_email = @current_account.contacts.find_by(email: email)
|
||||
if contact_with_email
|
||||
@contact = ::ContactMergeAction.new(
|
||||
account: @current_account,
|
||||
base_contact: contact_with_email,
|
||||
mergee_contact: @contact
|
||||
).perform
|
||||
else
|
||||
@contact.update!(email: email, name: contact_name)
|
||||
end
|
||||
end
|
||||
|
||||
def contact_email
|
||||
permitted_params[:contact][:email].downcase
|
||||
end
|
||||
|
||||
def contact_name
|
||||
params[:contact][:name] || contact_email.split('@')[0]
|
||||
end
|
||||
|
||||
def browser_params
|
||||
{
|
||||
browser_name: browser.name,
|
||||
@ -48,4 +88,19 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
platform_version: browser.platform.version
|
||||
}
|
||||
end
|
||||
|
||||
def timestamp_params
|
||||
{ timestamp: permitted_params[:message][:timestamp] }
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
sender: @contact,
|
||||
content: permitted_params[:message][:content],
|
||||
inbox_id: conversation.inbox_id,
|
||||
echo_id: permitted_params[:message][:echo_id],
|
||||
message_type: :incoming
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@ -12,7 +12,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController
|
||||
|
||||
def process_hmac
|
||||
return if params[:identifier_hash].blank?
|
||||
raise StandardError, 'HMAC failed: Invalid Identifer Hash Provided' unless valid_hmac?
|
||||
raise StandardError, 'HMAC failed: Invalid Identifier Hash Provided' unless valid_hmac?
|
||||
|
||||
@contact_inbox.update(hmac_verified: true)
|
||||
end
|
||||
|
||||
@ -5,6 +5,14 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
@conversation = conversation
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
update_contact(contact_email) if @contact.email.blank? && contact_email.present?
|
||||
@conversation = create_conversation
|
||||
conversation.messages.create(message_params)
|
||||
end
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
@ -43,6 +51,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :typing_status, :website_token, :email)
|
||||
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
end
|
||||
end
|
||||
|
||||
@ -39,44 +39,7 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@conversation = ::Conversation.create!(conversation_params) if conversation.nil?
|
||||
end
|
||||
|
||||
def message_params
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
sender: @contact,
|
||||
content: permitted_params[:message][:content],
|
||||
inbox_id: conversation.inbox_id,
|
||||
echo_id: permitted_params[:message][:echo_id],
|
||||
message_type: :incoming
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
# FIXME: typo referrer in additional attributes
|
||||
# will probably require a migration.
|
||||
{
|
||||
account_id: inbox.account_id,
|
||||
inbox_id: inbox.id,
|
||||
contact_id: @contact.id,
|
||||
contact_inbox_id: @contact_inbox.id,
|
||||
additional_attributes: {
|
||||
browser: browser_params,
|
||||
referer: permitted_params[:message][:referer_url],
|
||||
initiated_at: timestamp_params
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
def timestamp_params
|
||||
{
|
||||
timestamp: permitted_params[:message][:timestamp]
|
||||
}
|
||||
end
|
||||
|
||||
def inbox
|
||||
@inbox ||= ::Inbox.find_by(id: auth_token_params[:inbox_id])
|
||||
@conversation = create_conversation if conversation.nil?
|
||||
end
|
||||
|
||||
def message_finder_params
|
||||
@ -90,36 +53,12 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
@message_finder ||= MessageFinder.new(conversation, message_finder_params)
|
||||
end
|
||||
|
||||
def update_contact(email)
|
||||
contact_with_email = @current_account.contacts.find_by(email: email)
|
||||
if contact_with_email
|
||||
@contact = ::ContactMergeAction.new(
|
||||
account: @current_account,
|
||||
base_contact: contact_with_email,
|
||||
mergee_contact: @contact
|
||||
).perform
|
||||
else
|
||||
@contact.update!(
|
||||
email: email,
|
||||
name: contact_name
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def contact_email
|
||||
permitted_params[:contact][:email].downcase
|
||||
end
|
||||
|
||||
def contact_name
|
||||
contact_email.split('@')[0]
|
||||
end
|
||||
|
||||
def message_update_params
|
||||
params.permit(message: [{ submitted_values: [:name, :title, :value] }])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
params.permit(:id, :before, :website_token, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id])
|
||||
end
|
||||
|
||||
def set_message
|
||||
|
||||
26
app/controllers/concerns/twitter_concern.rb
Normal file
26
app/controllers/concerns/twitter_concern.rb
Normal file
@ -0,0 +1,26 @@
|
||||
module TwitterConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
private
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
|
||||
end
|
||||
|
||||
def host
|
||||
ENV.fetch('FRONTEND_URL', '')
|
||||
end
|
||||
|
||||
def twitter_client
|
||||
Twitty::Facade.new do |config|
|
||||
config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil)
|
||||
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
|
||||
config.base_url = twitter_api_base_url
|
||||
config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '')
|
||||
end
|
||||
end
|
||||
|
||||
def twitter_api_base_url
|
||||
'https://api.twitter.com'
|
||||
end
|
||||
end
|
||||
@ -20,7 +20,8 @@ class DashboardController < ActionController::Base
|
||||
'TERMS_URL',
|
||||
'PRIVACY_URL',
|
||||
'DISPLAY_MANIFEST',
|
||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD'
|
||||
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
|
||||
'CHATWOOT_INBOX_TOKEN'
|
||||
).merge(
|
||||
APP_VERSION: Chatwoot.config[:version]
|
||||
)
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
class Twitter::AuthorizationsController < Twitter::BaseController
|
||||
def create
|
||||
@response = twitter_client.request_oauth_token(url: twitter_callback_url)
|
||||
|
||||
if @response.status == '200'
|
||||
::Redis::Alfred.setex(oauth_token, account.id)
|
||||
redirect_to oauth_authorize_endpoint(oauth_token)
|
||||
else
|
||||
redirect_to app_new_twitter_inbox_url(account_id: account.id)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def oauth_token
|
||||
parsed_body['oauth_token']
|
||||
end
|
||||
|
||||
def user
|
||||
@user ||= User.find_by(id: params[:user_id])
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= user.account
|
||||
end
|
||||
|
||||
def oauth_authorize_endpoint(oauth_token)
|
||||
"#{twitter_api_base_url}/oauth/authorize?oauth_token=#{oauth_token}"
|
||||
end
|
||||
end
|
||||
@ -1,24 +1,3 @@
|
||||
class Twitter::BaseController < ApplicationController
|
||||
private
|
||||
|
||||
def parsed_body
|
||||
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
|
||||
end
|
||||
|
||||
def host
|
||||
ENV.fetch('FRONTEND_URL', '')
|
||||
end
|
||||
|
||||
def twitter_client
|
||||
Twitty::Facade.new do |config|
|
||||
config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil)
|
||||
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
|
||||
config.base_url = twitter_api_base_url
|
||||
config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '')
|
||||
end
|
||||
end
|
||||
|
||||
def twitter_api_base_url
|
||||
'https://api.twitter.com'
|
||||
end
|
||||
include TwitterConcern
|
||||
end
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
class Twitter::CallbacksController < Twitter::BaseController
|
||||
include TwitterConcern
|
||||
|
||||
def show
|
||||
return redirect_to twitter_app_redirect_url if permitted_params[:denied]
|
||||
|
||||
|
||||
@ -22,10 +22,12 @@ class ConversationFinder
|
||||
|
||||
def perform
|
||||
set_inboxes
|
||||
set_team
|
||||
set_assignee_type
|
||||
|
||||
find_all_conversations
|
||||
filter_by_status unless params[:q]
|
||||
filter_by_team if @team
|
||||
filter_by_labels if params[:labels]
|
||||
filter_by_query if params[:q]
|
||||
|
||||
@ -61,6 +63,10 @@ class ConversationFinder
|
||||
@assignee_type = params[:assignee_type]
|
||||
end
|
||||
|
||||
def set_team
|
||||
@team = current_account.teams.find(params[:team_id]) if params[:team_id]
|
||||
end
|
||||
|
||||
def find_all_conversations
|
||||
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
|
||||
end
|
||||
@ -87,6 +93,10 @@ class ConversationFinder
|
||||
@conversations = @conversations.where(status: params[:status] || DEFAULT_STATUS)
|
||||
end
|
||||
|
||||
def filter_by_team
|
||||
@conversations = @conversations.where(team: @team)
|
||||
end
|
||||
|
||||
def filter_by_labels
|
||||
@conversations = @conversations.tagged_with(params[:labels], any: true)
|
||||
end
|
||||
|
||||
@ -9,6 +9,8 @@ module FileTypeHelper
|
||||
'image/bmp'
|
||||
].include?(content_type)
|
||||
|
||||
return :audio if content_type.include?('audio/')
|
||||
|
||||
:file
|
||||
end
|
||||
end
|
||||
|
||||
14
app/javascript/dashboard/api/channel/twitterClient.js
Normal file
14
app/javascript/dashboard/api/channel/twitterClient.js
Normal file
@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class TwitterClient extends ApiClient {
|
||||
constructor() {
|
||||
super('twitter', { accountScoped: true });
|
||||
}
|
||||
|
||||
generateAuthorization() {
|
||||
return axios.post(`${this.url}/authorization`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new TwitterClient();
|
||||
@ -6,10 +6,11 @@ class ConversationApi extends ApiClient {
|
||||
super('conversations', { accountScoped: true });
|
||||
}
|
||||
|
||||
get({ inboxId, status, assigneeType, page, labels }) {
|
||||
get({ inboxId, status, assigneeType, page, labels, teamId }) {
|
||||
return axios.get(this.url, {
|
||||
params: {
|
||||
inbox_id: inboxId,
|
||||
team_id: teamId,
|
||||
status,
|
||||
assignee_type: assigneeType,
|
||||
page,
|
||||
@ -56,13 +57,14 @@ class ConversationApi extends ApiClient {
|
||||
return axios.post(`${this.url}/${conversationId}/unmute`);
|
||||
}
|
||||
|
||||
meta({ inboxId, status, assigneeType, labels }) {
|
||||
meta({ inboxId, status, assigneeType, labels, teamId }) {
|
||||
return axios.get(`${this.url}/meta`, {
|
||||
params: {
|
||||
inbox_id: inboxId,
|
||||
status,
|
||||
assignee_type: assigneeType,
|
||||
labels,
|
||||
team_id: teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
33
app/javascript/dashboard/api/notifications.js
Normal file
33
app/javascript/dashboard/api/notifications.js
Normal file
@ -0,0 +1,33 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class NotificationsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('notifications', { accountScoped: true });
|
||||
}
|
||||
|
||||
get(page) {
|
||||
return axios.get(`${this.url}?page=${page}`);
|
||||
}
|
||||
|
||||
getNotifications(contactId) {
|
||||
return axios.get(`${this.url}/${contactId}/notifications`);
|
||||
}
|
||||
|
||||
getUnreadCount() {
|
||||
return axios.get(`${this.url}/unread_count`);
|
||||
}
|
||||
|
||||
read(primaryActorType, primaryActorId) {
|
||||
return axios.post(`${this.url}/read_all`, {
|
||||
primary_actor_type: primaryActorType,
|
||||
primary_actor_id: primaryActorId,
|
||||
});
|
||||
}
|
||||
|
||||
readAll() {
|
||||
return axios.post(`${this.url}/read_all`);
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotificationsAPI();
|
||||
@ -0,0 +1,9 @@
|
||||
import TwitterClient from '../../channel/twitterClient';
|
||||
import ApiClient from '../../ApiClient';
|
||||
|
||||
describe('#TwitterClient', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(TwitterClient).toBeInstanceOf(ApiClient);
|
||||
expect(TwitterClient).toHaveProperty('generateAuthorization');
|
||||
});
|
||||
});
|
||||
13
app/javascript/dashboard/api/specs/notifications.spec.js
Normal file
13
app/javascript/dashboard/api/specs/notifications.spec.js
Normal file
@ -0,0 +1,13 @@
|
||||
import notifications from '../notifications';
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
describe('#NotificationAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(notifications).toBeInstanceOf(ApiClient);
|
||||
expect(notifications).toHaveProperty('get');
|
||||
expect(notifications).toHaveProperty('getNotifications');
|
||||
expect(notifications).toHaveProperty('getUnreadCount');
|
||||
expect(notifications).toHaveProperty('read');
|
||||
expect(notifications).toHaveProperty('readAll');
|
||||
});
|
||||
});
|
||||
15
app/javascript/dashboard/api/specs/teams.spec.js
Normal file
15
app/javascript/dashboard/api/specs/teams.spec.js
Normal file
@ -0,0 +1,15 @@
|
||||
import teams from '../teams';
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
describe('#TeamsAPI', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(teams).toBeInstanceOf(ApiClient);
|
||||
expect(teams).toHaveProperty('get');
|
||||
expect(teams).toHaveProperty('show');
|
||||
expect(teams).toHaveProperty('create');
|
||||
expect(teams).toHaveProperty('update');
|
||||
expect(teams).toHaveProperty('delete');
|
||||
expect(teams).toHaveProperty('getAgents');
|
||||
expect(teams).toHaveProperty('addAgents');
|
||||
});
|
||||
});
|
||||
20
app/javascript/dashboard/api/teams.js
Normal file
20
app/javascript/dashboard/api/teams.js
Normal file
@ -0,0 +1,20 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
export class TeamsAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('teams', { accountScoped: true });
|
||||
}
|
||||
|
||||
getAgents({ teamId }) {
|
||||
return axios.get(`${this.url}/${teamId}/team_members`);
|
||||
}
|
||||
|
||||
addAgents({ teamId, agentsList }) {
|
||||
return axios.post(`${this.url}/${teamId}/team_members`, {
|
||||
user_ids: agentsList,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new TeamsAPI();
|
||||
@ -8,23 +8,6 @@
|
||||
}
|
||||
|
||||
.reply-box__top {
|
||||
.canned {
|
||||
@include elegant-card;
|
||||
background: $color-white;
|
||||
border-bottom: $space-small solid $color-white;
|
||||
border-top: $space-small solid $color-white;
|
||||
left: 0;
|
||||
|
||||
max-height: 14rem;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
width: 24rem;
|
||||
z-index: 100;
|
||||
|
||||
.active a {
|
||||
background: $color-woot;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: $medium-gray;
|
||||
|
||||
@ -49,8 +49,10 @@
|
||||
margin-top: $space-micro;
|
||||
|
||||
.inbox-icon {
|
||||
$icon-top-space: -1px;
|
||||
display: inline-block;
|
||||
margin-right: $space-micro;
|
||||
margin-top: $icon-top-space;
|
||||
min-width: $space-normal;
|
||||
text-align: center;
|
||||
}
|
||||
@ -70,9 +72,9 @@
|
||||
.dropdown-pane {
|
||||
@include elegant-card;
|
||||
@include border-light;
|
||||
bottom: 6rem;
|
||||
display: block;
|
||||
left: 18%;
|
||||
top: -110px;
|
||||
left: 5rem;
|
||||
visibility: visible;
|
||||
width: 80%;
|
||||
z-index: 999;
|
||||
@ -118,42 +120,6 @@
|
||||
line-height: $global-lineheight;
|
||||
}
|
||||
|
||||
.current-user {
|
||||
@include flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
flex-direction: row;
|
||||
|
||||
.current-user--data {
|
||||
@include flex;
|
||||
flex-direction: column;
|
||||
|
||||
.current-user--name {
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-medium;
|
||||
line-height: 1;
|
||||
margin-bottom: $space-smaller;
|
||||
margin-left: $space-one;
|
||||
margin-top: $space-micro;
|
||||
}
|
||||
|
||||
.current-user--role {
|
||||
color: $color-gray;
|
||||
font-size: $font-size-mini;
|
||||
margin-bottom: $zero;
|
||||
margin-left: $space-one;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
||||
.current-user--options {
|
||||
font-size: $font-size-big;
|
||||
margin-bottom: auto;
|
||||
margin-left: auto;
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.hamburger--menu {
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
|
||||
@ -80,6 +80,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
activeTeam: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -128,12 +132,16 @@ export default {
|
||||
status: this.activeStatus,
|
||||
page: this.currentPage + 1,
|
||||
labels: this.label ? [this.label] : undefined,
|
||||
teamId: this.activeTeam.name ? this.activeTeam.id : undefined,
|
||||
};
|
||||
},
|
||||
pageTitle() {
|
||||
if (this.inbox.name) {
|
||||
return this.inbox.name;
|
||||
}
|
||||
if (this.activeTeam.name) {
|
||||
return this.activeTeam.name;
|
||||
}
|
||||
if (this.label) {
|
||||
return `#${this.label}`;
|
||||
}
|
||||
@ -162,6 +170,9 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeTeam() {
|
||||
this.resetAndFetchData();
|
||||
},
|
||||
conversationInbox() {
|
||||
this.resetAndFetchData();
|
||||
},
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<button class="button small button--copy-code" @click="onCopy">
|
||||
{{ $t('COMPONENTS.CODE.BUTTON_TEXT') }}
|
||||
</button>
|
||||
<highlightjs :language="lang" :code="script" />
|
||||
<highlightjs v-if="script" :language="lang" :code="script" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -15,7 +15,7 @@ export default {
|
||||
props: {
|
||||
script: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
lang: {
|
||||
type: String,
|
||||
|
||||
@ -13,6 +13,11 @@
|
||||
:key="item.toState"
|
||||
:menu-item="item"
|
||||
/>
|
||||
<sidebar-item
|
||||
v-if="shouldShowTeams"
|
||||
:key="teamSection.toState"
|
||||
:menu-item="teamSection"
|
||||
/>
|
||||
<sidebar-item
|
||||
v-if="shouldShowInboxes"
|
||||
:key="inboxSection.toState"
|
||||
@ -30,173 +35,65 @@
|
||||
<availability-status />
|
||||
</div>
|
||||
|
||||
<div class="bottom-nav app-context-menu">
|
||||
<transition name="menu-slide">
|
||||
<div
|
||||
v-if="showOptionsMenu"
|
||||
v-on-clickaway="showOptions"
|
||||
class="dropdown-pane top"
|
||||
>
|
||||
<ul class="vertical dropdown menu">
|
||||
<li v-if="showChangeAccountOption">
|
||||
<button
|
||||
class="button clear change-accounts--button"
|
||||
@click="changeAccount"
|
||||
>
|
||||
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="`/app/accounts/${accountId}/profile/settings`">
|
||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" @click.prevent="logout()">
|
||||
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<div class="current-user" @click.prevent="showOptions()">
|
||||
<thumbnail
|
||||
:src="currentUser.avatar_url"
|
||||
:username="currentUserAvailableName"
|
||||
/>
|
||||
<div class="current-user--data">
|
||||
<h3 class="current-user--name">
|
||||
{{ currentUserAvailableName }}
|
||||
</h3>
|
||||
<h5 v-if="currentRole" class="current-user--role">
|
||||
{{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }}
|
||||
</h5>
|
||||
</div>
|
||||
<span class="current-user--options icon ion-android-more-vertical" />
|
||||
</div>
|
||||
</div>
|
||||
<woot-modal
|
||||
:show="showAccountModal"
|
||||
:on-close="onClose"
|
||||
class="account-selector--modal"
|
||||
>
|
||||
<woot-modal-header
|
||||
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
||||
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
||||
<div class="bottom-nav app-context-menu" @click="toggleOptions">
|
||||
<agent-details @show-options="toggleOptions" />
|
||||
<notification-bell />
|
||||
<span class="current-user--options icon ion-android-more-vertical" />
|
||||
<options-menu
|
||||
:show="showOptionsMenu"
|
||||
@toggle-accounts="toggleAccountModal"
|
||||
@show-support-chat-window="toggleSupportChatWindow"
|
||||
@close="toggleOptions"
|
||||
/>
|
||||
<div
|
||||
v-for="account in currentUser.accounts"
|
||||
:key="account.id"
|
||||
class="account-selector"
|
||||
>
|
||||
<a :href="`/app/accounts/${account.id}/dashboard`">
|
||||
<i v-if="account.id === accountId" class="ion ion-ios-checkmark" />
|
||||
<label :for="account.name" class="account--details">
|
||||
<div class="account--name">{{ account.name }}</div>
|
||||
<div class="account--role">{{ account.role }}</div>
|
||||
</label>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="globalConfig.createNewAccountFromDashboard"
|
||||
class="modal-footer delete-item"
|
||||
>
|
||||
<button
|
||||
class="button success large expanded nice"
|
||||
@click="createAccount"
|
||||
>
|
||||
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
||||
</button>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</div>
|
||||
|
||||
<woot-modal
|
||||
<account-selector
|
||||
:show-account-modal="showAccountModal"
|
||||
@close-account-modal="toggleAccountModal"
|
||||
@show-create-account-modal="openCreateAccountModal"
|
||||
/>
|
||||
|
||||
<add-account-modal
|
||||
:show="showCreateAccountModal"
|
||||
:on-close="onCloseCreate"
|
||||
class="account-selector--modal"
|
||||
>
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
||||
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
|
||||
<form class="row" @submit.prevent="addAccount()">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.accountName.$error }">
|
||||
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="accountName"
|
||||
type="text"
|
||||
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
|
||||
@input="$v.accountName.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-footer medium-12 columns">
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:disabled="
|
||||
$v.accountName.$invalid ||
|
||||
$v.accountName.$invalid ||
|
||||
uiFlags.isCreating
|
||||
"
|
||||
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isCreating"
|
||||
button-class="large expanded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</woot-modal>
|
||||
@close-account-create-modal="closeCreateAccountModal"
|
||||
/>
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
|
||||
import adminMixin from '../../mixins/isAdmin';
|
||||
import Auth from '../../api/auth';
|
||||
import SidebarItem from './SidebarItem';
|
||||
import AvailabilityStatus from './AvailabilityStatus';
|
||||
import { frontendURL } from '../../helper/URLHelper';
|
||||
import Thumbnail from '../widgets/Thumbnail';
|
||||
import { getSidebarItems } from '../../i18n/default-sidebar';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import NotificationBell from './sidebarComponents/NotificationBell';
|
||||
import AgentDetails from './sidebarComponents/AgentDetails.vue';
|
||||
import OptionsMenu from './sidebarComponents/OptionsMenu.vue';
|
||||
import AccountSelector from './sidebarComponents/AccountSelector.vue';
|
||||
import AddAccountModal from './sidebarComponents/AddAccountModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AgentDetails,
|
||||
SidebarItem,
|
||||
Thumbnail,
|
||||
AvailabilityStatus,
|
||||
NotificationBell,
|
||||
OptionsMenu,
|
||||
AccountSelector,
|
||||
AddAccountModal,
|
||||
},
|
||||
mixins: [clickaway, adminMixin, alertMixin],
|
||||
props: {
|
||||
route: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
mixins: [adminMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
showOptionsMenu: false,
|
||||
showAccountModal: false,
|
||||
showCreateAccountModal: false,
|
||||
accountName: '',
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
accountName: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
@ -204,18 +101,10 @@ export default {
|
||||
inboxes: 'inboxes/getInboxes',
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentRole: 'getCurrentRole',
|
||||
uiFlags: 'agents/getUIFlags',
|
||||
accountLabels: 'labels/getLabelsOnSidebar',
|
||||
teams: 'teams/getMyTeams',
|
||||
}),
|
||||
currentUserAvailableName() {
|
||||
return this.currentUser.name;
|
||||
},
|
||||
showChangeAccountOption() {
|
||||
if (this.globalConfig.createNewAccountFromDashboard) {
|
||||
return true;
|
||||
}
|
||||
return this.currentUser.accounts.length > 1;
|
||||
},
|
||||
|
||||
sidemenuItems() {
|
||||
return getSidebarItems(this.accountId);
|
||||
},
|
||||
@ -242,6 +131,9 @@ export default {
|
||||
shouldShowInboxes() {
|
||||
return this.sidemenuItems.common.routes.includes(this.currentRoute);
|
||||
},
|
||||
shouldShowTeams() {
|
||||
return this.shouldShowInboxes && this.teams.length;
|
||||
},
|
||||
inboxSection() {
|
||||
return {
|
||||
icon: 'ion-folder',
|
||||
@ -281,15 +173,57 @@ export default {
|
||||
})),
|
||||
};
|
||||
},
|
||||
teamSection() {
|
||||
return {
|
||||
icon: 'ion-ios-people',
|
||||
label: 'TEAMS',
|
||||
hasSubMenu: true,
|
||||
newLink: true,
|
||||
key: 'team',
|
||||
cssClass: 'menu-title align-justify teams-sidebar-menu',
|
||||
toState: frontendURL(`accounts/${this.accountId}/settings/teams`),
|
||||
toStateName: 'teams_list',
|
||||
children: this.teams.map(team => ({
|
||||
id: team.id,
|
||||
label: team.name,
|
||||
truncateLabel: true,
|
||||
toState: frontendURL(`accounts/${this.accountId}/team/${team.id}`),
|
||||
})),
|
||||
};
|
||||
},
|
||||
dashboardPath() {
|
||||
return frontendURL(`accounts/${this.accountId}/dashboard`);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentUser(newUserInfo, oldUserInfo) {
|
||||
if (!oldUserInfo.email && newUserInfo.email) {
|
||||
this.setChatwootUser();
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$store.dispatch('labels/get');
|
||||
this.$store.dispatch('inboxes/get');
|
||||
this.$store.dispatch('notifications/unReadCount');
|
||||
this.$store.dispatch('teams/get');
|
||||
this.setChatwootUser();
|
||||
},
|
||||
methods: {
|
||||
toggleSupportChatWindow() {
|
||||
window.$chatwoot.toggle();
|
||||
},
|
||||
setChatwootUser() {
|
||||
if (!this.currentUser.email || !this.globalConfig.chatwootInboxToken) {
|
||||
return;
|
||||
}
|
||||
window.$chatwoot.setUser(this.currentUser.email, {
|
||||
name: this.currentUser.name,
|
||||
email: this.currentUser.email,
|
||||
avatar_url: this.currentUser.avatar_url,
|
||||
identifier_hash: this.currentUser.hmac_identifier,
|
||||
});
|
||||
},
|
||||
filterMenuItemsByRole(menuItems) {
|
||||
if (!this.currentRole) {
|
||||
return [];
|
||||
@ -301,41 +235,19 @@ export default {
|
||||
) > -1
|
||||
);
|
||||
},
|
||||
logout() {
|
||||
Auth.logout();
|
||||
},
|
||||
showOptions() {
|
||||
toggleOptions() {
|
||||
this.showOptionsMenu = !this.showOptionsMenu;
|
||||
},
|
||||
changeAccount() {
|
||||
this.showAccountModal = true;
|
||||
toggleAccountModal() {
|
||||
this.showAccountModal = !this.showAccountModal;
|
||||
},
|
||||
onClose() {
|
||||
this.showAccountModal = false;
|
||||
},
|
||||
createAccount() {
|
||||
openCreateAccountModal() {
|
||||
this.showAccountModal = false;
|
||||
this.showCreateAccountModal = true;
|
||||
},
|
||||
onCloseCreate() {
|
||||
closeCreateAccountModal() {
|
||||
this.showCreateAccountModal = false;
|
||||
},
|
||||
async addAccount() {
|
||||
try {
|
||||
const account_id = await this.$store.dispatch('accounts/create', {
|
||||
account_name: this.accountName,
|
||||
});
|
||||
this.onClose();
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
||||
window.location = `/app/accounts/${account_id}/dashboard`;
|
||||
} catch (error) {
|
||||
if (error.response.status === 422) {
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
||||
} else {
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -406,7 +318,23 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.app-context-menu {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
.current-user--options {
|
||||
font-size: $font-size-big;
|
||||
margin-bottom: auto;
|
||||
margin-left: auto;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.teams-sidebar-menu + .nested.vertical.menu {
|
||||
padding-left: calc(var(--space-medium) - var(--space-one));
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -155,7 +155,6 @@ export default {
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label-color--display {
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<woot-modal
|
||||
:show="showAccountModal"
|
||||
:on-close="() => $emit('close-account-modal')"
|
||||
class="account-selector--modal"
|
||||
>
|
||||
<woot-modal-header
|
||||
:header-title="$t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS')"
|
||||
:header-content="$t('SIDEBAR_ITEMS.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
<div
|
||||
v-for="account in currentUser.accounts"
|
||||
:key="account.id"
|
||||
class="account-selector"
|
||||
>
|
||||
<a :href="`/app/accounts/${account.id}/dashboard`">
|
||||
<i v-if="account.id === accountId" class="ion ion-ios-checkmark" />
|
||||
<label :for="account.name" class="account--details">
|
||||
<div class="account--name">{{ account.name }}</div>
|
||||
<div class="account--role">{{ account.role }}</div>
|
||||
</label>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
v-if="globalConfig.createNewAccountFromDashboard"
|
||||
class="modal-footer delete-item"
|
||||
>
|
||||
<button
|
||||
class="button success large expanded nice"
|
||||
@click="$emit('show-create-account-modal')"
|
||||
>
|
||||
{{ $t('CREATE_ACCOUNT.NEW_ACCOUNT') }}
|
||||
</button>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
props: {
|
||||
showAccountModal: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
}),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<woot-modal
|
||||
:show="show"
|
||||
:on-close="() => $emit('close-account-create-modal')"
|
||||
class="account-selector--modal"
|
||||
>
|
||||
<div class="column content-box">
|
||||
<woot-modal-header
|
||||
:header-title="$t('CREATE_ACCOUNT.NEW_ACCOUNT')"
|
||||
:header-content="$t('CREATE_ACCOUNT.SELECTOR_SUBTITLE')"
|
||||
/>
|
||||
|
||||
<form class="row" @submit.prevent="addAccount">
|
||||
<div class="medium-12 columns">
|
||||
<label :class="{ error: $v.accountName.$error }">
|
||||
{{ $t('CREATE_ACCOUNT.FORM.NAME.LABEL') }}
|
||||
<input
|
||||
v-model.trim="accountName"
|
||||
type="text"
|
||||
:placeholder="$t('CREATE_ACCOUNT.FORM.NAME.PLACEHOLDER')"
|
||||
@input="$v.accountName.$touch"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div class="modal-footer medium-12 columns">
|
||||
<div class="medium-12 columns">
|
||||
<woot-submit-button
|
||||
:disabled="
|
||||
$v.accountName.$invalid ||
|
||||
$v.accountName.$invalid ||
|
||||
uiFlags.isCreating
|
||||
"
|
||||
:button-text="$t('CREATE_ACCOUNT.FORM.SUBMIT')"
|
||||
:loading="uiFlags.isCreating"
|
||||
button-class="large expanded"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</woot-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
mixins: [alertMixin],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
accountName: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
accountName: {
|
||||
required,
|
||||
minLength: minLength(1),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
uiFlags: 'agents/getUIFlags',
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
async addAccount() {
|
||||
try {
|
||||
const account_id = await this.$store.dispatch('accounts/create', {
|
||||
account_name: this.accountName,
|
||||
});
|
||||
this.$emit('close-account-create-modal');
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.SUCCESS_MESSAGE'));
|
||||
window.location = `/app/accounts/${account_id}/dashboard`;
|
||||
} catch (error) {
|
||||
if (error.response.status === 422) {
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.EXIST_MESSAGE'));
|
||||
} else {
|
||||
this.showAlert(this.$t('CREATE_ACCOUNT.API.ERROR_MESSAGE'));
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="current-user--row">
|
||||
<thumbnail
|
||||
:src="currentUser.avatar_url"
|
||||
:username="currentUserAvailableName"
|
||||
/>
|
||||
<div class="current-user--data">
|
||||
<h3 class="current-user--name text-truncate">
|
||||
{{ currentUserAvailableName }}
|
||||
</h3>
|
||||
<h5 v-if="currentRole" class="current-user--role">
|
||||
{{ $t(`AGENT_MGMT.AGENT_TYPES.${currentRole.toUpperCase()}`) }}
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import Thumbnail from '../../widgets/Thumbnail';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
currentRole: 'getCurrentRole',
|
||||
}),
|
||||
currentUserAvailableName() {
|
||||
return this.currentUser.name;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.current-user--row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.current-user--data {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.current-user--name {
|
||||
font-size: var(--font-size-small);
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin-bottom: var(--space-micro);
|
||||
margin-left: var(--space-one);
|
||||
max-width: 12rem;
|
||||
}
|
||||
|
||||
.current-user--role {
|
||||
color: var(--color-gray);
|
||||
font-size: var(--font-size-mini);
|
||||
margin-bottom: var(--zero);
|
||||
margin-left: var(--space-one);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<span class="notifications icon ion-ios-bell" @click.stop="showNotification">
|
||||
<span v-if="unreadCount" class="unread-badge">{{ unreadCount }}</span>
|
||||
</span>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
export default {
|
||||
computed: {
|
||||
...mapGetters({
|
||||
accountId: 'getCurrentAccountId',
|
||||
notificationMetadata: 'notifications/getMeta',
|
||||
}),
|
||||
unreadCount() {
|
||||
if (!this.notificationMetadata.unreadCount) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.notificationMetadata.unreadCount < 100
|
||||
? this.notificationMetadata.unreadCount
|
||||
: '99+';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
showNotification() {
|
||||
this.$router.push(`/app/accounts/${this.accountId}/notifications`);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.notifications {
|
||||
font-size: var(--font-size-big);
|
||||
margin-bottom: auto;
|
||||
margin-left: auto;
|
||||
margin-top: auto;
|
||||
position: relative;
|
||||
|
||||
.unread-badge {
|
||||
background: var(--r-300);
|
||||
border-radius: var(--space-small);
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-micro);
|
||||
font-weight: var(--font-weight-black);
|
||||
left: var(--space-slab);
|
||||
padding: 0 var(--space-smaller);
|
||||
position: absolute;
|
||||
top: var(--space-smaller);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<transition name="menu-slide">
|
||||
<div
|
||||
v-if="show"
|
||||
v-on-clickaway="() => $emit('close')"
|
||||
class="dropdown-pane top"
|
||||
>
|
||||
<ul class="vertical dropdown menu">
|
||||
<li v-if="showChangeAccountOption">
|
||||
<button
|
||||
class="button clear change-accounts--button"
|
||||
@click="$emit('toggle-accounts')"
|
||||
>
|
||||
{{ $t('SIDEBAR_ITEMS.CHANGE_ACCOUNTS') }}
|
||||
</button>
|
||||
</li>
|
||||
<li v-if="globalConfig.chatwootInboxToken">
|
||||
<button
|
||||
class="button clear change-accounts--button"
|
||||
@click="$emit('show-support-chat-window')"
|
||||
>
|
||||
Contact Support
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="`/app/accounts/${accountId}/profile/settings`">
|
||||
{{ $t('SIDEBAR_ITEMS.PROFILE_SETTINGS') }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" @click.prevent="logout">
|
||||
{{ $t('SIDEBAR_ITEMS.LOGOUT') }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mixin as clickaway } from 'vue-clickaway';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Auth from '../../../api/auth';
|
||||
|
||||
export default {
|
||||
mixins: [clickaway],
|
||||
props: {
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
globalConfig: 'globalConfig/get',
|
||||
accountId: 'getCurrentAccountId',
|
||||
}),
|
||||
showChangeAccountOption() {
|
||||
if (this.globalConfig.createNewAccountFromDashboard) {
|
||||
return true;
|
||||
}
|
||||
return this.currentUser.accounts.length > 1;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
logout() {
|
||||
Auth.logout();
|
||||
|
||||
if (this.globalConfig.chatwootInboxToken) {
|
||||
window.$chatwoot.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -172,6 +172,7 @@ export default {
|
||||
margin-left: -2px;
|
||||
font-size: var(--font-size-small);
|
||||
padding: var(--space-small) var(--space-normal);
|
||||
border-radius: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
@ -1,55 +1,113 @@
|
||||
<template>
|
||||
<div ref="editor" class="editor-root"></div>
|
||||
<div class="editor-root">
|
||||
<tag-agents
|
||||
v-if="showUserMentions && isPrivate"
|
||||
:search-key="mentionSearchKey"
|
||||
@click="insertMentionNode"
|
||||
/>
|
||||
<div ref="editor"></div>
|
||||
</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';
|
||||
import { defaultMarkdownSerializer } from 'prosemirror-markdown';
|
||||
|
||||
const TYPING_INDICATOR_IDLE_TIME = 4000;
|
||||
|
||||
const createState = (content, placeholder) =>
|
||||
EditorState.create({
|
||||
doc: defaultMarkdownParser.parse(content),
|
||||
plugins: wootWriterSetup({ schema, placeholder }),
|
||||
import {
|
||||
addMentionsToMarkdownSerializer,
|
||||
addMentionsToMarkdownParser,
|
||||
schemaWithMentions,
|
||||
} from '@chatwoot/prosemirror-schema/src/mentions/schema';
|
||||
import {
|
||||
suggestionsPlugin,
|
||||
triggerCharacters,
|
||||
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
|
||||
import TagAgents from '../conversation/TagAgents.vue';
|
||||
import { EditorState } from 'prosemirror-state';
|
||||
import { defaultMarkdownParser } from 'prosemirror-markdown';
|
||||
import { wootWriterSetup } from '@chatwoot/prosemirror-schema';
|
||||
|
||||
const createState = (content, placeholder, plugins = []) => {
|
||||
return EditorState.create({
|
||||
doc: addMentionsToMarkdownParser(defaultMarkdownParser).parse(content),
|
||||
plugins: wootWriterSetup({
|
||||
schema: schemaWithMentions,
|
||||
placeholder,
|
||||
plugins,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'WootMessageEditor',
|
||||
components: { TagAgents },
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
isPrivate: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastValue: null,
|
||||
showUserMentions: false,
|
||||
mentionSearchKey: '',
|
||||
editorView: null,
|
||||
range: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
plugins() {
|
||||
return [
|
||||
suggestionsPlugin({
|
||||
matcher: triggerCharacters('@'),
|
||||
onEnter: args => {
|
||||
this.showUserMentions = true;
|
||||
this.range = args.range;
|
||||
this.editorView = args.view;
|
||||
return false;
|
||||
},
|
||||
onChange: args => {
|
||||
this.editorView = args.view;
|
||||
this.range = args.range;
|
||||
|
||||
this.mentionSearchKey = args.text.replace('@', '');
|
||||
return false;
|
||||
},
|
||||
onExit: () => {
|
||||
this.mentionSearchKey = '';
|
||||
this.showUserMentions = false;
|
||||
this.editorView = null;
|
||||
return false;
|
||||
},
|
||||
onKeyDown: ({ event }) => {
|
||||
return event.keyCode === 13 && this.showUserMentions;
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
showUserMentions(updatedValue) {
|
||||
this.$emit('toggle-user-mention', this.isPrivate && updatedValue);
|
||||
},
|
||||
value(newValue) {
|
||||
if (newValue !== this.lastValue) {
|
||||
this.state = createState(newValue, this.placeholder);
|
||||
this.state = createState(newValue, this.placeholder, this.plugins);
|
||||
this.view.updateState(this.state);
|
||||
}
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.state = createState(this.value, this.placeholder);
|
||||
this.state = createState(this.value, this.placeholder, this.plugins);
|
||||
},
|
||||
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);
|
||||
this.emitOnChange();
|
||||
},
|
||||
handleDOMEvents: {
|
||||
keyup: () => {
|
||||
@ -65,6 +123,33 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
insertMentionNode(mentionItem) {
|
||||
if (!this.view) {
|
||||
return null;
|
||||
}
|
||||
const node = this.view.state.schema.nodes.mention.create({
|
||||
userId: mentionItem.key,
|
||||
userFullName: mentionItem.label,
|
||||
});
|
||||
|
||||
const tr = this.view.state.tr.replaceWith(
|
||||
this.range.from,
|
||||
this.range.to,
|
||||
node
|
||||
);
|
||||
this.state = this.view.state.apply(tr);
|
||||
return this.emitOnChange();
|
||||
},
|
||||
emitOnChange() {
|
||||
this.view.updateState(this.state);
|
||||
this.lastValue = addMentionsToMarkdownSerializer(
|
||||
defaultMarkdownSerializer
|
||||
).serialize(this.state.doc);
|
||||
this.$emit('input', this.lastValue);
|
||||
},
|
||||
hideMentions() {
|
||||
this.showUserMentions = false;
|
||||
},
|
||||
resetTyping() {
|
||||
this.$emit('typing-off');
|
||||
this.idleTimer = null;
|
||||
@ -115,4 +200,14 @@ export default {
|
||||
max-height: 12rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.is-private {
|
||||
.prosemirror-mention-node {
|
||||
font-weight: var(--font-weight-medium);
|
||||
background: var(--s-300);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: 1px 4px;
|
||||
color: var(--white);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
</file-upload>
|
||||
</button>
|
||||
<button
|
||||
v-if="enableRichEditor"
|
||||
v-if="enableRichEditor && !isOnPrivateNote"
|
||||
class="button clear button--emoji"
|
||||
:title="$t('CONVERSATION.REPLYBOX.TIP_FORMAT_ICON')"
|
||||
@click="toggleFormatMode"
|
||||
@ -31,6 +31,17 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="right-wrap">
|
||||
<div v-if="isFormatMode" class="enter-to-send--checkbox">
|
||||
<input
|
||||
:checked="enterToSendEnabled"
|
||||
type="checkbox"
|
||||
value="enterToSend"
|
||||
@input="toggleEnterToSend"
|
||||
/>
|
||||
<label for="enterToSend">
|
||||
{{ $t('CONVERSATION.REPLYBOX.ENTER_TO_SEND') }}
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
class="button nice primary button--send"
|
||||
:class="buttonClass"
|
||||
@ -91,10 +102,18 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isOnPrivateNote: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enableRichEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
enterToSendEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isNote() {
|
||||
@ -119,6 +138,9 @@ export default {
|
||||
toggleFormatMode() {
|
||||
this.setFormatMode(!this.isFormatMode);
|
||||
},
|
||||
toggleEnterToSend() {
|
||||
this.$emit('toggleEnterToSend', !this.enterToSendEnabled);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -173,8 +195,8 @@ export default {
|
||||
}
|
||||
|
||||
.left-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.button--reply {
|
||||
@ -185,4 +207,21 @@ export default {
|
||||
color: var(--s-600);
|
||||
font-size: var(--font-size-default);
|
||||
}
|
||||
|
||||
.right-wrap {
|
||||
display: flex;
|
||||
|
||||
.enter-to-send--checkbox {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
input {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--s-500);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,88 +1,45 @@
|
||||
<template>
|
||||
<ul
|
||||
v-if="cannedMessages.length"
|
||||
id="canned-list"
|
||||
class="vertical dropdown menu canned"
|
||||
:style="{ top: getTopPadding() + 'rem' }"
|
||||
>
|
||||
<li
|
||||
v-for="(item, index) in cannedMessages"
|
||||
:id="`canned-${index}`"
|
||||
:key="item.short_code"
|
||||
:class="{ active: index === selectedIndex }"
|
||||
@click="onListItemSelection(index)"
|
||||
@mouseover="onHover(index)"
|
||||
>
|
||||
<a class="text-truncate">
|
||||
<strong>{{ item.short_code }}</strong> - {{ item.content }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<mention-box :items="items" @mention-select="handleMentionClick" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MentionBox from '../mentions/MentionBox.vue';
|
||||
|
||||
export default {
|
||||
props: ['onKeyenter', 'onClick'],
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: 0,
|
||||
};
|
||||
components: { MentionBox },
|
||||
props: {
|
||||
searchKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
cannedMessages: 'getCannedResponses',
|
||||
}),
|
||||
items() {
|
||||
return this.cannedMessages.map(cannedMessage => ({
|
||||
label: cannedMessage.short_code,
|
||||
key: cannedMessage.short_code,
|
||||
description: cannedMessage.content,
|
||||
}));
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
searchKey() {
|
||||
this.fetchCannedResponses();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.keyListener);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.keyListener);
|
||||
this.fetchCannedResponses();
|
||||
},
|
||||
methods: {
|
||||
getTopPadding() {
|
||||
if (this.cannedMessages.length <= 4) {
|
||||
return -this.cannedMessages.length * 3.5;
|
||||
}
|
||||
return -14;
|
||||
fetchCannedResponses() {
|
||||
this.$store.dispatch('getCannedResponse', { searchKey: this.searchKey });
|
||||
},
|
||||
isUp(e) {
|
||||
return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
|
||||
},
|
||||
isDown(e) {
|
||||
return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
|
||||
},
|
||||
isEnter(e) {
|
||||
return e.keyCode === 13;
|
||||
},
|
||||
keyListener(e) {
|
||||
if (this.isUp(e)) {
|
||||
if (!this.selectedIndex) {
|
||||
this.selectedIndex = this.cannedMessages.length - 1;
|
||||
} else {
|
||||
this.selectedIndex -= 1;
|
||||
}
|
||||
}
|
||||
if (this.isDown(e)) {
|
||||
if (this.selectedIndex === this.cannedMessages.length - 1) {
|
||||
this.selectedIndex = 0;
|
||||
} else {
|
||||
this.selectedIndex += 1;
|
||||
}
|
||||
}
|
||||
if (this.isEnter(e)) {
|
||||
this.onKeyenter(this.cannedMessages[this.selectedIndex].content);
|
||||
}
|
||||
this.$el.scrollTop = 34 * this.selectedIndex;
|
||||
},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
},
|
||||
onListItemSelection(index) {
|
||||
this.selectedIndex = index;
|
||||
this.onClick(this.cannedMessages[this.selectedIndex].content);
|
||||
handleMentionClick(item = {}) {
|
||||
this.$emit('click', item.description);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -35,6 +35,12 @@
|
||||
{{ this.$t(`${attachmentIconKey}.CONTENT`) }}
|
||||
</span>
|
||||
</p>
|
||||
<p v-else class="conversation--message">
|
||||
<i class="ion-android-alert"></i>
|
||||
<span>
|
||||
{{ this.$t(`CHAT_LIST.NO_MESSAGES`) }}
|
||||
</span>
|
||||
</p>
|
||||
<div class="conversation--meta">
|
||||
<span class="timestamp">
|
||||
{{ dynamicTime(chat.timestamp) }}
|
||||
|
||||
@ -21,8 +21,11 @@
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<audio v-else-if="attachment.file_type === 'audio'" controls>
|
||||
<source :src="attachment.data_url" />
|
||||
</audio>
|
||||
<bubble-file
|
||||
v-if="attachment.file_type !== 'image'"
|
||||
v-else
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
@ -72,7 +75,7 @@ import Spinner from 'shared/components/Spinner';
|
||||
import contentTypeMixin from 'shared/mixins/contentTypeMixin';
|
||||
import BubbleActions from './bubble/Actions';
|
||||
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||
|
||||
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
|
||||
export default {
|
||||
components: {
|
||||
BubbleActions,
|
||||
@ -99,7 +102,19 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
message() {
|
||||
return this.formatMessage(this.data.content, this.isATweet);
|
||||
const botMessageContent = generateBotMessageContent(
|
||||
this.contentType,
|
||||
this.contentAttributes,
|
||||
this.$t('CONVERSATION.NO_RESPONSE')
|
||||
);
|
||||
let messageContent =
|
||||
this.formatMessage(this.data.content, this.isATweet) +
|
||||
botMessageContent;
|
||||
|
||||
return messageContent;
|
||||
},
|
||||
contentAttributes() {
|
||||
return this.data.content_attributes || {};
|
||||
},
|
||||
sender() {
|
||||
return this.data.sender || {};
|
||||
@ -119,7 +134,7 @@ export default {
|
||||
return !this.data.message_type ? 'left' : 'right';
|
||||
},
|
||||
readableTime() {
|
||||
return this.messageStamp(this.data.created_at);
|
||||
return this.messageStamp(this.data.created_at, 'LLL d, h:mm a');
|
||||
},
|
||||
isBubble() {
|
||||
return [0, 1, 3].includes(this.data.message_type);
|
||||
@ -193,6 +208,22 @@ export default {
|
||||
max-width: 32rem;
|
||||
padding: var(--space-small) var(--space-normal);
|
||||
}
|
||||
|
||||
&.is-private .file.message-text__wrap {
|
||||
.ion-document-text {
|
||||
color: var(--w-400);
|
||||
}
|
||||
.text-block-title {
|
||||
color: #3c4858;
|
||||
}
|
||||
.download.button {
|
||||
color: var(--w-400);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-private.is-text > .message-text__wrap .link {
|
||||
color: var(--w-700);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-pending {
|
||||
|
||||
@ -8,11 +8,10 @@
|
||||
/>
|
||||
<div class="reply-box__top">
|
||||
<canned-response
|
||||
v-if="showCannedResponsesList"
|
||||
v-on-clickaway="hideCannedResponse"
|
||||
data-dropdown-menu
|
||||
:on-keyenter="replaceText"
|
||||
:on-click="replaceText"
|
||||
v-if="showMentions && hasSlashCommand"
|
||||
v-on-clickaway="hideMentions"
|
||||
:search-key="mentionSearchKey"
|
||||
@click="replaceText"
|
||||
/>
|
||||
<emoji-input
|
||||
v-if="showEmojiPicker"
|
||||
@ -20,7 +19,7 @@
|
||||
:on-click="emojiOnClick"
|
||||
/>
|
||||
<resizable-text-area
|
||||
v-if="!isFormatMode"
|
||||
v-if="!showRichContentEditor"
|
||||
ref="messageInput"
|
||||
v-model="message"
|
||||
class="input"
|
||||
@ -35,12 +34,14 @@
|
||||
v-else
|
||||
v-model="message"
|
||||
class="input"
|
||||
:is-private="isOnPrivateNote"
|
||||
:placeholder="messagePlaceHolder"
|
||||
:min-height="4"
|
||||
@typing-off="onTypingOff"
|
||||
@typing-on="onTypingOn"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@toggle-user-mention="toggleUserMention"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasAttachments" class="attachment-preview-box">
|
||||
@ -59,8 +60,11 @@
|
||||
:on-send="sendMessage"
|
||||
:is-send-disabled="isReplyButtonDisabled"
|
||||
:set-format-mode="setFormatMode"
|
||||
:is-format-mode="isFormatMode"
|
||||
:is-on-private-note="isOnPrivateNote"
|
||||
:is-format-mode="showRichContentEditor"
|
||||
:enable-rich-editor="isRichEditorEnabled"
|
||||
:enter-to-send-enabled="enterToSendEnabled"
|
||||
@toggleEnterToSend="toggleEnterToSend"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -84,6 +88,7 @@ import {
|
||||
} from 'shared/helpers/KeyboardHelpers';
|
||||
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
|
||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -95,7 +100,7 @@ export default {
|
||||
ReplyBottomPanel,
|
||||
WootMessageEditor,
|
||||
},
|
||||
mixins: [clickaway, inboxMixin],
|
||||
mixins: [clickaway, inboxMixin, uiSettingsMixin],
|
||||
props: {
|
||||
inReplyTo: {
|
||||
type: [String, Number],
|
||||
@ -107,18 +112,29 @@ export default {
|
||||
message: '',
|
||||
isFocused: false,
|
||||
showEmojiPicker: false,
|
||||
showCannedResponsesList: false,
|
||||
showMentions: false,
|
||||
attachedFiles: [],
|
||||
isUploading: false,
|
||||
replyType: REPLY_EDITOR_MODES.REPLY,
|
||||
isFormatMode: false,
|
||||
mentionSearchKey: '',
|
||||
hasUserMention: false,
|
||||
hasSlashCommand: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
showRichContentEditor() {
|
||||
const {
|
||||
display_rich_content_editor: displayRichContentEditor,
|
||||
} = this.uiSettings;
|
||||
return this.isOnPrivateNote || displayRichContentEditor;
|
||||
},
|
||||
...mapGetters({ currentChat: 'getSelectedChat' }),
|
||||
enterToSendEnabled() {
|
||||
return !!this.uiSettings.enter_to_send_enabled;
|
||||
},
|
||||
isPrivate() {
|
||||
if (this.currentChat.can_reply) {
|
||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||
return this.isOnPrivateNote;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
@ -200,15 +216,20 @@ export default {
|
||||
},
|
||||
isRichEditorEnabled() {
|
||||
return (
|
||||
this.isAWebWidgetInbox ||
|
||||
this.isAnEmailChannel ||
|
||||
this.replyType === REPLY_EDITOR_MODES.NOTE
|
||||
this.isAWebWidgetInbox || this.isAnEmailChannel || this.isOnPrivateNote
|
||||
);
|
||||
},
|
||||
isOnPrivateNote() {
|
||||
return this.replyType === REPLY_EDITOR_MODES.NOTE;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentChat(conversation) {
|
||||
const { can_reply: canReply } = conversation;
|
||||
if (this.isOnPrivateNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canReply) {
|
||||
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
||||
} else {
|
||||
@ -216,22 +237,15 @@ export default {
|
||||
}
|
||||
},
|
||||
message(updatedMessage) {
|
||||
if (this.isPrivate) {
|
||||
return;
|
||||
}
|
||||
const isSlashCommand = updatedMessage[0] === '/';
|
||||
this.hasSlashCommand = updatedMessage[0] === '/';
|
||||
const hasNextWord = updatedMessage.includes(' ');
|
||||
const isShortCodeActive = isSlashCommand && !hasNextWord;
|
||||
const isShortCodeActive = this.hasSlashCommand && !hasNextWord;
|
||||
if (isShortCodeActive) {
|
||||
this.showCannedResponsesList = true;
|
||||
if (updatedMessage.length > 1) {
|
||||
const searchKey = updatedMessage.substr(1, updatedMessage.length);
|
||||
this.$store.dispatch('getCannedResponse', { searchKey });
|
||||
} else {
|
||||
this.$store.dispatch('getCannedResponse');
|
||||
}
|
||||
this.mentionSearchKey = updatedMessage.substr(1, updatedMessage.length);
|
||||
this.showMentions = true;
|
||||
} else {
|
||||
this.showCannedResponsesList = false;
|
||||
this.mentionSearchKey = '';
|
||||
this.showMentions = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -242,24 +256,35 @@ export default {
|
||||
document.removeEventListener('keydown', this.handleKeyEvents);
|
||||
},
|
||||
methods: {
|
||||
toggleUserMention(currentMentionState) {
|
||||
this.hasUserMention = currentMentionState;
|
||||
},
|
||||
handleKeyEvents(e) {
|
||||
if (isEscape(e)) {
|
||||
this.hideEmojiPicker();
|
||||
this.hideCannedResponse();
|
||||
this.hideMentions();
|
||||
} else if (isEnter(e)) {
|
||||
const hasSendOnEnterEnabled =
|
||||
(this.showRichContentEditor &&
|
||||
this.enterToSendEnabled &&
|
||||
!this.hasUserMention) ||
|
||||
!this.showRichContentEditor;
|
||||
const shouldSendMessage =
|
||||
!this.isFormatMode && !hasPressedShift(e) && this.isFocused;
|
||||
hasSendOnEnterEnabled && !hasPressedShift(e) && this.isFocused;
|
||||
if (shouldSendMessage) {
|
||||
e.preventDefault();
|
||||
this.sendMessage();
|
||||
}
|
||||
}
|
||||
},
|
||||
toggleEnterToSend(enterToSendEnabled) {
|
||||
this.updateUISettings({ enter_to_send_enabled: enterToSendEnabled });
|
||||
},
|
||||
async sendMessage() {
|
||||
if (this.isReplyButtonDisabled) {
|
||||
return;
|
||||
}
|
||||
if (!this.showCannedResponsesList) {
|
||||
if (!this.showMentions) {
|
||||
const newMessage = this.message;
|
||||
const messagePayload = this.getMessagePayload(newMessage);
|
||||
this.clearMessage();
|
||||
@ -281,7 +306,10 @@ export default {
|
||||
const { can_reply: canReply } = this.currentChat;
|
||||
|
||||
if (canReply) this.replyType = mode;
|
||||
this.$refs.messageInput.focus();
|
||||
if (this.showRichContentEditor) {
|
||||
return;
|
||||
}
|
||||
this.$nextTick(() => this.$refs.messageInput.focus());
|
||||
},
|
||||
emojiOnClick(emoji) {
|
||||
this.message = `${this.message}${emoji} `;
|
||||
@ -298,8 +326,8 @@ export default {
|
||||
this.toggleEmojiPicker();
|
||||
}
|
||||
},
|
||||
hideCannedResponse() {
|
||||
this.showCannedResponsesList = false;
|
||||
hideMentions() {
|
||||
this.showMentions = false;
|
||||
},
|
||||
onTypingOn() {
|
||||
this.toggleTyping('on');
|
||||
@ -363,7 +391,7 @@ export default {
|
||||
return messagePayload;
|
||||
},
|
||||
setFormatMode(value) {
|
||||
this.isFormatMode = value;
|
||||
this.updateUISettings({ display_rich_content_editor: value });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<mention-box :items="items" @mention-select="handleMentionClick" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import MentionBox from '../mentions/MentionBox.vue';
|
||||
|
||||
export default {
|
||||
components: { MentionBox },
|
||||
props: {
|
||||
searchKey: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
agents: 'agents/getVerifiedAgents',
|
||||
}),
|
||||
items() {
|
||||
if (!this.searchKey) {
|
||||
return this.agents.map(agent => ({
|
||||
label: agent.name,
|
||||
key: agent.id,
|
||||
description: agent.email,
|
||||
}));
|
||||
}
|
||||
|
||||
return this.agents
|
||||
.filter(agent =>
|
||||
agent.name
|
||||
.toLocaleLowerCase()
|
||||
.includes(this.searchKey.toLocaleLowerCase())
|
||||
)
|
||||
.map(agent => ({
|
||||
label: agent.name,
|
||||
key: agent.id,
|
||||
description: agent.email,
|
||||
}));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleMentionClick(item = {}) {
|
||||
this.$emit('click', item);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,57 @@
|
||||
const generateInputSelectContent = (contentType, contentAttributes) => {
|
||||
const { submitted_values: submittedValues = [] } = contentAttributes;
|
||||
const [selectedOption] = submittedValues;
|
||||
|
||||
if (selectedOption && selectedOption.title) {
|
||||
return `<strong>${selectedOption.title}</strong>`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const generateInputEmailContent = (contentType, contentAttributes) => {
|
||||
const { submitted_email: submittedEmail = '' } = contentAttributes;
|
||||
if (submittedEmail) {
|
||||
return `<strong>${submittedEmail}</strong>`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const generateFormContent = (
|
||||
contentType,
|
||||
contentAttributes,
|
||||
noResponseText
|
||||
) => {
|
||||
const { items, submitted_values: submittedValues = [] } = contentAttributes;
|
||||
if (submittedValues.length) {
|
||||
const submittedObject = submittedValues.reduce((acc, keyValuePair) => {
|
||||
acc[keyValuePair.name] = keyValuePair.value;
|
||||
return acc;
|
||||
}, {});
|
||||
let formMessageContent = '';
|
||||
items.forEach(item => {
|
||||
formMessageContent += `<div>${item.label}</div>`;
|
||||
const response = submittedObject[item.name] || noResponseText;
|
||||
formMessageContent += `<strong>${response}</strong><br/><br/>`;
|
||||
});
|
||||
return formMessageContent;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const generateBotMessageContent = (
|
||||
contentType,
|
||||
contentAttributes,
|
||||
noResponseText = 'No response'
|
||||
) => {
|
||||
const contentTypeMethods = {
|
||||
input_select: generateInputSelectContent,
|
||||
input_email: generateInputEmailContent,
|
||||
form: generateFormContent,
|
||||
};
|
||||
|
||||
const contentTypeMethod = contentTypeMethods[contentType];
|
||||
if (contentTypeMethod && typeof contentTypeMethod === 'function') {
|
||||
return contentTypeMethod(contentType, contentAttributes, noResponseText);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
@ -0,0 +1,39 @@
|
||||
import { generateBotMessageContent } from '../botMessageContentHelper';
|
||||
|
||||
describe('#generateBotMessageContent', () => {
|
||||
it('return correct input_select content', () => {
|
||||
expect(
|
||||
generateBotMessageContent('input_select', {
|
||||
submitted_values: [{ value: 'salad', title: 'Salad' }],
|
||||
})
|
||||
).toEqual('<strong>Salad</strong>');
|
||||
});
|
||||
|
||||
it('return correct input_email content', () => {
|
||||
expect(
|
||||
generateBotMessageContent('input_email', {
|
||||
submitted_email: 'hello@chatwoot.com',
|
||||
})
|
||||
).toEqual('<strong>hello@chatwoot.com</strong>');
|
||||
});
|
||||
|
||||
it('return correct form content', () => {
|
||||
expect(
|
||||
generateBotMessageContent('form', {
|
||||
items: [
|
||||
{
|
||||
name: 'large_text',
|
||||
label: 'This a large text',
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
label: 'Email',
|
||||
},
|
||||
],
|
||||
submitted_values: [{ name: 'large_text', value: 'Large Text Content' }],
|
||||
})
|
||||
).toEqual(
|
||||
'<div>This a large text</div><strong>Large Text Content</strong><br/><br/><div>Email</div><strong>No response</strong><br/><br/>'
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<ul
|
||||
v-if="items.length"
|
||||
class="vertical dropdown menu mention--box"
|
||||
:style="{ top: getTopPadding() + 'rem' }"
|
||||
>
|
||||
<li
|
||||
v-for="(item, index) in items"
|
||||
:id="`mention-item-${index}`"
|
||||
:key="item.key"
|
||||
:class="{ active: index === selectedIndex }"
|
||||
@click="onListItemSelection(index)"
|
||||
@mouseover="onHover(index)"
|
||||
>
|
||||
<a class="text-truncate">
|
||||
<strong>{{ item.label }}</strong> - {{ item.description }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIndex: 0,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
items(newItems) {
|
||||
if (newItems.length < this.selectedIndex + 1) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.keyListener);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.keyListener);
|
||||
},
|
||||
methods: {
|
||||
getTopPadding() {
|
||||
if (this.items.length <= 4) {
|
||||
return -(this.items.length * 2.8 + 1.7);
|
||||
}
|
||||
return -14;
|
||||
},
|
||||
isUp(e) {
|
||||
return e.keyCode === 38 || (e.ctrlKey && e.keyCode === 80); // UP, Ctrl-P
|
||||
},
|
||||
isDown(e) {
|
||||
return e.keyCode === 40 || (e.ctrlKey && e.keyCode === 78); // DOWN, Ctrl-N
|
||||
},
|
||||
isEnter(e) {
|
||||
return e.keyCode === 13;
|
||||
},
|
||||
keyListener(e) {
|
||||
if (this.isUp(e)) {
|
||||
if (!this.selectedIndex) {
|
||||
this.selectedIndex = this.items.length - 1;
|
||||
} else {
|
||||
this.selectedIndex -= 1;
|
||||
}
|
||||
}
|
||||
if (this.isDown(e)) {
|
||||
if (this.selectedIndex === this.items.length - 1) {
|
||||
this.selectedIndex = 0;
|
||||
} else {
|
||||
this.selectedIndex += 1;
|
||||
}
|
||||
}
|
||||
if (this.isEnter(e)) {
|
||||
this.onMentionSelect();
|
||||
}
|
||||
this.$el.scrollTop = 28 * this.selectedIndex;
|
||||
},
|
||||
onHover(index) {
|
||||
this.selectedIndex = index;
|
||||
},
|
||||
onListItemSelection(index) {
|
||||
this.selectedIndex = index;
|
||||
this.onMentionSelect();
|
||||
},
|
||||
onMentionSelect() {
|
||||
this.$emit('mention-select', this.items[this.selectedIndex]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.mention--box {
|
||||
background: var(--white);
|
||||
border-bottom: var(--space-small) solid var(--white);
|
||||
border-top: 1px solid var(--color-border);
|
||||
left: 0;
|
||||
max-height: 14rem;
|
||||
overflow: auto;
|
||||
padding-top: var(--space-small);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
|
||||
.active a {
|
||||
background: var(--w-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -8,11 +8,15 @@ export const getSidebarItems = accountId => ({
|
||||
'inbox_conversation',
|
||||
'conversation_through_inbox',
|
||||
'contacts_dashboard',
|
||||
'notifications_dashboard',
|
||||
'settings_account_reports',
|
||||
'profile_settings',
|
||||
'profile_settings_index',
|
||||
'label_conversations',
|
||||
'conversations_through_label',
|
||||
'team_conversations',
|
||||
'conversations_through_team',
|
||||
'notifications_index',
|
||||
],
|
||||
menuItems: {
|
||||
assignedToMe: {
|
||||
@ -31,6 +35,13 @@ export const getSidebarItems = accountId => ({
|
||||
toState: frontendURL(`accounts/${accountId}/contacts`),
|
||||
toStateName: 'contacts_dashboard',
|
||||
},
|
||||
notifications: {
|
||||
icon: 'ion-ios-bell',
|
||||
label: 'NOTIFICATIONS',
|
||||
hasSubMenu: false,
|
||||
toState: frontendURL(`accounts/${accountId}/notifications`),
|
||||
toStateName: 'notifications_dashboard',
|
||||
},
|
||||
report: {
|
||||
icon: 'ion-arrow-graph-up-right',
|
||||
label: 'REPORTS',
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "تم تلقيه عبر البريد الإلكتروني",
|
||||
"VIEW_TWEET_IN_TWITTER": "عرض التغريدة في تويتر",
|
||||
"REPLY_TO_TWEET": "الرد على هذه التغريدة"
|
||||
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
|
||||
"NO_MESSAGES": "No Messages"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "إزالة التحديد",
|
||||
"DOWNLOAD": "تنزيل",
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"NO_RESPONSE": "No response",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "إغلاق المحادثة",
|
||||
"REOPEN_ACTION": "إعادة فتح",
|
||||
@ -44,7 +45,8 @@
|
||||
"TWEET": "تغريد",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "ملاحظة خاصة: مرئية فقط لأعضاء فريق العمل والموظفين",
|
||||
"CHANGE_STATUS": "تم تغيير حالة المحادثة",
|
||||
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"SELECT_ONE": "Select one"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "الإشعارات",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"TABLE_HEADER": [
|
||||
"الاسم",
|
||||
"رقم الهاتف",
|
||||
"المحادثات",
|
||||
"Last Contacted"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message",
|
||||
"conversation_mention": "Mention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "قم بتحديث تفضيلات إشعار البريد الإلكتروني الخاص بك من هنا",
|
||||
"CONVERSATION_ASSIGNMENT": "إرسال إشعارات البريد الإلكتروني عند إسناد محادثة لي",
|
||||
"CONVERSATION_CREATION": "إرسال إشعارات للبريد الإلكتروني عند ورود محادثة جديدة",
|
||||
"CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "قم بتحديث تفضيلات إشعارات المتصفح من هنا",
|
||||
"CONVERSATION_ASSIGNMENT": "إرسال إشعارات على المتصفح عند إسناد محادثة لي",
|
||||
"CONVERSATION_CREATION": "إرسال إشعارات المتصفح عند ورود محادثة جديدة",
|
||||
"CONVERSATION_MENTION": "Send push notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation",
|
||||
"HAS_ENABLED_PUSH": "لقد قمت بتمكين الإشعارات لهذا المتصفح.",
|
||||
"REQUEST_PUSH": "تفعيل إشعارات المتصفح"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "الرئيسية",
|
||||
"AGENTS": "موظف الدعم",
|
||||
"INBOXES": "قنوات التواصل",
|
||||
"NOTIFICATIONS": "الإشعارات",
|
||||
"CANNED_RESPONSES": "الردود السريعة",
|
||||
"INTEGRATIONS": "خيارات الربط",
|
||||
"ACCOUNT_SETTINGS": "إعدادات الحساب",
|
||||
"LABELS": "الوسوم"
|
||||
"LABELS": "الوسوم",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "حساب جديد",
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Rebut per correu electrònic",
|
||||
"VIEW_TWEET_IN_TWITTER": "Veure el tuit a Twitter",
|
||||
"REPLY_TO_TWEET": "Respon a aquest tuit"
|
||||
"REPLY_TO_TWEET": "Respon a aquest tuit",
|
||||
"NO_MESSAGES": "Cap Missatge"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "Elimina la selecció",
|
||||
"DOWNLOAD": "Descarrega",
|
||||
"UPLOADING_ATTACHMENTS": "Pujant fitxers adjunts...",
|
||||
"NO_RESPONSE": "Sense resposta",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resoldre",
|
||||
"REOPEN_ACTION": "Tornar a obrir",
|
||||
@ -42,9 +43,10 @@
|
||||
"SEND": "Envia",
|
||||
"CREATE": "Afegeix una nota",
|
||||
"TWEET": "Tuit",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_FORMAT_ICON": "Mostra l'editor de text enriquit",
|
||||
"TIP_EMOJI_ICON": "Mostra la selecció d'emoticones",
|
||||
"TIP_ATTACH_ICON": "Ajuntar fitxers",
|
||||
"ENTER_TO_SEND": "Intro per enviar"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip",
|
||||
"CHANGE_STATUS": "Estat de la conversa canviat",
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"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."
|
||||
"UPDATE_CHATWOOT": "L'actualització %{latestChatwootVersion} per Chatwoot està disponible. Si us plau, actualitza l'instancia."
|
||||
},
|
||||
"FORMS": {
|
||||
"MULTISELECT": {
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Presiona retorn (tecla enter) per eliminar",
|
||||
"SELECT_ONE": "Selecciona un"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Notificacions",
|
||||
"MARK_ALL_DONE": "Marca Tot Fet",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Carregant notificacions...",
|
||||
"404": "Cap Notificació",
|
||||
"TABLE_HEADER": [
|
||||
"Nom",
|
||||
"Número de telèfon",
|
||||
"Converses",
|
||||
"Darrer contacte"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "Nova conversació",
|
||||
"conversation_assignment": "Conversació Assignada",
|
||||
"assigned_conversation_new_message": "Missatge Nou",
|
||||
"conversation_mention": "Menció"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,7 +242,7 @@
|
||||
"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",
|
||||
"HMAC_VERIFICATION": "User Identity Validation",
|
||||
"HMAC_VERIFICATION": "Validació de la Identitat del Usuari",
|
||||
"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": {
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "Actualitza aqui les preferències de les notificacions per correu electrònic",
|
||||
"CONVERSATION_ASSIGNMENT": "Envieu notificacions per correu electrònic quan se m’assigni una conversa",
|
||||
"CONVERSATION_CREATION": "Envieu notificacions per correu electrònic quan es crea una nova conversa",
|
||||
"CONVERSATION_MENTION": "Enviar notificacions per mail quan siguis esmentat en una conversació",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Envia notificacions per correu electrònic quan es creï un missatge nou en una conversa assignada"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "Actualitzeu les preferències de notificació aquí",
|
||||
"CONVERSATION_ASSIGNMENT": "Envia notificacions automàtiques quan se m'assigna una conversa",
|
||||
"CONVERSATION_CREATION": "Envia notificacions automàtiques quan es creï una conversa nova",
|
||||
"CONVERSATION_MENTION": "Enviar notificacions push quan siguis esmentat en una conversació",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Envia notificacions automàtiques quan es creï un missatge nou en una conversa assignada",
|
||||
"HAS_ENABLED_PUSH": "Heu activat les notificacions per a aquest navegador.",
|
||||
"REQUEST_PUSH": "Activa les notificacions"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "Inici",
|
||||
"AGENTS": "Agents",
|
||||
"INBOXES": "Safates d'entrada",
|
||||
"NOTIFICATIONS": "Notificacions",
|
||||
"CANNED_RESPONSES": "Respostes predeterminades",
|
||||
"INTEGRATIONS": "Integracions",
|
||||
"ACCOUNT_SETTINGS": "Configuració del compte",
|
||||
"LABELS": "Etiquetes"
|
||||
"LABELS": "Etiquetes",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "Compte nou",
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Obdrženo e-mailem",
|
||||
"VIEW_TWEET_IN_TWITTER": "Zobrazit tweet na Twitteru",
|
||||
"REPLY_TO_TWEET": "Odpovědět na tento tweet"
|
||||
"REPLY_TO_TWEET": "Odpovědět na tento tweet",
|
||||
"NO_MESSAGES": "No Messages"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "Odstranit výběr",
|
||||
"DOWNLOAD": "Stáhnout",
|
||||
"UPLOADING_ATTACHMENTS": "Nahrávání příloh...",
|
||||
"NO_RESPONSE": "No response",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Vyřešit",
|
||||
"REOPEN_ACTION": "Znovu otevřít",
|
||||
@ -44,7 +45,8 @@
|
||||
"TWEET": "Tweet",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Soukromá poznámka: Viditelné pouze pro vás a váš tým",
|
||||
"CHANGE_STATUS": "Stav konverzace byl změněn",
|
||||
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Stiskněte Enter pro odebrání",
|
||||
"SELECT_ONE": "Vyberte jeden"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Notifications",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"TABLE_HEADER": [
|
||||
"Název",
|
||||
"Telefonní číslo",
|
||||
"Konverzace",
|
||||
"Naposledy kontaktováno"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message",
|
||||
"conversation_mention": "Mention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "Zde aktualizujte nastavení e-mailových oznámení",
|
||||
"CONVERSATION_ASSIGNMENT": "Odeslat e-mailová oznámení, když je mi přiřazena konverzace",
|
||||
"CONVERSATION_CREATION": "Odeslat oznámení e-mailem při vytváření nové konverzace",
|
||||
"CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "Update your push notification preferences here",
|
||||
"CONVERSATION_ASSIGNMENT": "Send push notifications when a conversation is assigned to me",
|
||||
"CONVERSATION_CREATION": "Send push notifications when a new conversation is created",
|
||||
"CONVERSATION_MENTION": "Send push notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation",
|
||||
"HAS_ENABLED_PUSH": "You have enabled push for this browser.",
|
||||
"REQUEST_PUSH": "Enable push notifications"
|
||||
@ -98,7 +100,7 @@
|
||||
},
|
||||
"FILE_BUBBLE": {
|
||||
"DOWNLOAD": "Stáhnout",
|
||||
"UPLOADING": "Uploading..."
|
||||
"UPLOADING": "Nahrávání..."
|
||||
},
|
||||
"FORM_BUBBLE": {
|
||||
"SUBMIT": "Odeslat"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Agenti",
|
||||
"INBOXES": "Krabice",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"CANNED_RESPONSES": "Konzervované odpovědi",
|
||||
"INTEGRATIONS": "Integrace",
|
||||
"ACCOUNT_SETTINGS": "Account Settings",
|
||||
"LABELS": "Labels"
|
||||
"LABELS": "Labels",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "New Account",
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Modtaget via e-mail",
|
||||
"VIEW_TWEET_IN_TWITTER": "Se tweet på Twitter",
|
||||
"REPLY_TO_TWEET": "Svar på dette tweet"
|
||||
"REPLY_TO_TWEET": "Svar på dette tweet",
|
||||
"NO_MESSAGES": "No Messages"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "Fjern Markering",
|
||||
"DOWNLOAD": "Download",
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"NO_RESPONSE": "No response",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Løs",
|
||||
"REOPEN_ACTION": "Genåben",
|
||||
@ -44,7 +45,8 @@
|
||||
"TWEET": "Tweet",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Privat Note: Kun synlig for dig og dit team",
|
||||
"CHANGE_STATUS": "Samtalestatus ændret",
|
||||
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"SELECT_ONE": "Select one"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Notifications",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"TABLE_HEADER": [
|
||||
"Navn",
|
||||
"Telefonnummer",
|
||||
"Samtaler",
|
||||
"Sidst Kontaktet"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message",
|
||||
"conversation_mention": "Mention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "Opdater dine e-mail notifikationspræferencer her",
|
||||
"CONVERSATION_ASSIGNMENT": "Send e-mail notifikationer når en samtale er tildelt mig",
|
||||
"CONVERSATION_CREATION": "Send e-mail notifikationer når en ny samtale er oprettet",
|
||||
"CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send e-mail notifikationer når en ny besked er oprettet i en tildelt samtale"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "Opdater dine push-notifikationspræferencer her",
|
||||
"CONVERSATION_ASSIGNMENT": "Send push-notifikationer, når en samtale er tildelt mig",
|
||||
"CONVERSATION_CREATION": "Send push-notifikationer, når en ny samtale er oprettet",
|
||||
"CONVERSATION_MENTION": "Send push notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push-notifikationer, når en ny besked oprettes i en tildelt samtale",
|
||||
"HAS_ENABLED_PUSH": "Du har aktiveret push for denne browser.",
|
||||
"REQUEST_PUSH": "Aktivér push-notifikationer"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "Hjem",
|
||||
"AGENTS": "Agenter",
|
||||
"INBOXES": "Indbakker",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"CANNED_RESPONSES": "Standardsvar Svar",
|
||||
"INTEGRATIONS": "Integrationer",
|
||||
"ACCOUNT_SETTINGS": "Kontoindstillinger",
|
||||
"LABELS": "Etiketter"
|
||||
"LABELS": "Etiketter",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "Ny Konto",
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Per E-Mail empfangen",
|
||||
"VIEW_TWEET_IN_TWITTER": "Tweet auf Twitter anzeigen",
|
||||
"REPLY_TO_TWEET": "Auf diesen Tweet antworten"
|
||||
"REPLY_TO_TWEET": "Auf diesen Tweet antworten",
|
||||
"NO_MESSAGES": "No Messages"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "Auswahl entfernen",
|
||||
"DOWNLOAD": "Herunterladen",
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"NO_RESPONSE": "No response",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Fall schließen",
|
||||
"REOPEN_ACTION": "Wieder öffnen",
|
||||
@ -44,7 +45,8 @@
|
||||
"TWEET": "Tweet",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Privater Hinweis: Nur für Sie und Ihr Team sichtbar",
|
||||
"CHANGE_STATUS": "Gesprächsstatus geändert",
|
||||
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Drücke Enter zum Entfernen",
|
||||
"SELECT_ONE": "Eine Option wählen"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Notifications",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Telefonnummer",
|
||||
"Gespräche",
|
||||
"Letzter Kontakt"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message",
|
||||
"conversation_mention": "Mention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "Aktualisieren Sie hier Ihre E-Mail-Benachrichtigungseinstellungen",
|
||||
"CONVERSATION_ASSIGNMENT": "Senden Sie E-Mail-Benachrichtigungen, wenn mir ein Gespräch zugewiesen wurde",
|
||||
"CONVERSATION_CREATION": "Senden Sie E-Mail-Benachrichtigungen, wenn eine neue Konversation erstellt wird",
|
||||
"CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "Aktualisieren Sie hier Ihre Push-Benachrichtigungseinstellungen",
|
||||
"CONVERSATION_ASSIGNMENT": "Push-Benachrichtigungen senden, wenn mir ein Gespräch zugewiesen wird",
|
||||
"CONVERSATION_CREATION": "Push-Benachrichtigungen senden, wenn eine neue Konversation startet",
|
||||
"CONVERSATION_MENTION": "Send push notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation",
|
||||
"HAS_ENABLED_PUSH": "Sie haben die Push-Benachrichtigung für diesen Browser aktiviert.",
|
||||
"REQUEST_PUSH": "Push-Benachrichtigungen aktivieren"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "Hauptseite",
|
||||
"AGENTS": "Agenten",
|
||||
"INBOXES": "Posteingänge",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"CANNED_RESPONSES": "Vorgefertigte Antworten",
|
||||
"INTEGRATIONS": "Integrationen",
|
||||
"ACCOUNT_SETTINGS": "Kontoeinstellungen",
|
||||
"LABELS": "Labels"
|
||||
"LABELS": "Labels",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "New Account",
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Παραλήφθηκε από email",
|
||||
"VIEW_TWEET_IN_TWITTER": "Προβολή του tweet στο Twitter",
|
||||
"REPLY_TO_TWEET": "Απάντηση στο tweet"
|
||||
"REPLY_TO_TWEET": "Απάντηση στο tweet",
|
||||
"NO_MESSAGES": "Κανένα Μήνυμα"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "Διαγραφή Επιλογής",
|
||||
"DOWNLOAD": "Κατέβασμα",
|
||||
"UPLOADING_ATTACHMENTS": "Ανέβασμα επισυναπτόμενων...",
|
||||
"NO_RESPONSE": "Καμία ανταπόκριση",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Επίλυση",
|
||||
"REOPEN_ACTION": "Επαναφορά",
|
||||
@ -42,9 +43,10 @@
|
||||
"SEND": "Αποστολή",
|
||||
"CREATE": "Σημείωση (Add Note)",
|
||||
"TWEET": "Tweet",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_FORMAT_ICON": "Προβολή επεξεργαστή εμπλουτισμένου κειμένου",
|
||||
"TIP_EMOJI_ICON": "Προβολή επιλογέα emoji",
|
||||
"TIP_ATTACH_ICON": "Επισύναψη αρχείων",
|
||||
"ENTER_TO_SEND": "Εισαγωγή για αποστολή"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Ιδιωτική Σημείωση: Ορατή μόνο σε σας και την ομάδα σας",
|
||||
"CHANGE_STATUS": "Η κατάσταση της συνομιλίας άλλαξε",
|
||||
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Πάτησε enter για αφαίρεση",
|
||||
"SELECT_ONE": "Επιλέξτε ένα"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Ειδοποιήσεις",
|
||||
"MARK_ALL_DONE": "Όλες ολοκληρωμένες",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Φόρτωση ειδοποιήσεων...",
|
||||
"404": "Καμία Ειδοποίηση",
|
||||
"TABLE_HEADER": [
|
||||
"Όνομα",
|
||||
"Αριθμός Τηλεφώνου",
|
||||
"Συζητήσεις",
|
||||
"Τελευταία επικοινωνία"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "Νέα συνομιλία",
|
||||
"conversation_assignment": "Η Συνομιλία Ανατέθηκε",
|
||||
"assigned_conversation_new_message": "Νέο Μήνυμα",
|
||||
"conversation_mention": "Αναφορά"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "Ενημέρωστε την προτίμηση για ειδοποιήσεις με email εδώ",
|
||||
"CONVERSATION_ASSIGNMENT": "Να στέλνεται ειδοποίηση email όταν μια συνομιλία αντιστοιχίζεται σε μένα",
|
||||
"CONVERSATION_CREATION": "Να στέλνεται ειδοποίηση όταν δημιουργείται μια νέα συνομιλία",
|
||||
"CONVERSATION_MENTION": "Αποστολή ειδοποιήσεων email όταν αναφέρεστε σε μια συνομιλία",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Αποστολή ειδοποίησης email όταν ένα νέο μήνυμα δημιουργείται σε συνομιλία που έχει αναληφθεί"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "Ενημερώστε τις ρυθμίσεις για τις push ειδοποιήσεις εδώ",
|
||||
"CONVERSATION_ASSIGNMENT": "Να ειδοποιούμαι με push όταν μια συνομιλία μου ανατείθεται",
|
||||
"CONVERSATION_CREATION": "Να ειδοποιούμαι με push όταν δημιουργείται μια νέα συνομιλία",
|
||||
"CONVERSATION_MENTION": "Αποστολή ειδοποιήσεων push όταν αναφέρεστε σε μια συνομιλία",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Αποστολή ειδοποίησης push όταν ένα νέο μήνυμα δημιουργείται σε συνομιλία που έχει αναληφθεί",
|
||||
"HAS_ENABLED_PUSH": "Έχετε ενεργοποιήσει τις ειδοποιήσεις push για αυτόν τον browser.",
|
||||
"REQUEST_PUSH": "Ενεργοποποίηση των ειδοποιήσεων push"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "Αρχική",
|
||||
"AGENTS": "Πράκτορες",
|
||||
"INBOXES": "Κιβώτια Εισερχομένων",
|
||||
"NOTIFICATIONS": "Ειδοποιήσεις",
|
||||
"CANNED_RESPONSES": "Έτοιμες Απαντήσεις",
|
||||
"INTEGRATIONS": "Ενοποιήσεις",
|
||||
"ACCOUNT_SETTINGS": "Ρυθμίσεις Λογαριασμού",
|
||||
"LABELS": "Ετικέτες"
|
||||
"LABELS": "Ετικέτες",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "Νέος Λογαριασμός",
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Received via email",
|
||||
"VIEW_TWEET_IN_TWITTER": "View tweet in Twitter",
|
||||
"REPLY_TO_TWEET": "Reply to this tweet"
|
||||
"REPLY_TO_TWEET": "Reply to this tweet",
|
||||
"NO_MESSAGES": "No Messages"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "Remove Selection",
|
||||
"DOWNLOAD": "Download",
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"NO_RESPONSE": "No response",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resolve",
|
||||
"REOPEN_ACTION": "Reopen",
|
||||
@ -44,7 +45,8 @@
|
||||
"TWEET": "Tweet",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Private Note: Only visible to you and your team",
|
||||
"CHANGE_STATUS": "Conversation status changed",
|
||||
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"SELECT_ONE": "Select one"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Notifications",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"TABLE_HEADER": [
|
||||
"Name",
|
||||
"Phone Number",
|
||||
"Conversations",
|
||||
"Last Contacted"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message",
|
||||
"conversation_mention": "Mention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,7 +38,8 @@
|
||||
"PICK_A_VALUE": "Pick a value"
|
||||
},
|
||||
"TWITTER": {
|
||||
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' "
|
||||
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to Twitter, please try again"
|
||||
},
|
||||
"WEBSITE_CHANNEL": {
|
||||
"TITLE": "Website channel",
|
||||
@ -224,7 +225,8 @@
|
||||
"TABS": {
|
||||
"SETTINGS": "Settings",
|
||||
"COLLABORATORS": "Collaborators",
|
||||
"CONFIGURATION": "Configuration"
|
||||
"CONFIGURATION": "Configuration",
|
||||
"PRE_CHAT_FORM": "Pre Chat Form"
|
||||
},
|
||||
"SETTINGS": "Settings",
|
||||
"FEATURES": {
|
||||
@ -250,6 +252,23 @@
|
||||
"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"
|
||||
},
|
||||
"PRE_CHAT_FORM": {
|
||||
"DESCRIPTION": "Pre chat forms enable you to capture user information before they start conversation with you.",
|
||||
"ENABLE": {
|
||||
"LABEL": "Enable pre chat form",
|
||||
"OPTIONS": {
|
||||
"ENABLED": "Yes",
|
||||
"DISABLED": "No"
|
||||
}
|
||||
},
|
||||
"PRE_CHAT_MESSAGE": {
|
||||
"LABEL": "Pre Chat Message",
|
||||
"PLACEHOLDER": "This message would be visible to the users along with the form"
|
||||
},
|
||||
"REQUIRE_EMAIL": {
|
||||
"LABEL": "Visitors should provide their name and email address before starting the chat"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "Update your email notification preferences here",
|
||||
"CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me",
|
||||
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created",
|
||||
"CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "Update your push notification preferences here",
|
||||
"CONVERSATION_ASSIGNMENT": "Send push notifications when a conversation is assigned to me",
|
||||
"CONVERSATION_CREATION": "Send push notifications when a new conversation is created",
|
||||
"CONVERSATION_MENTION": "Send push notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation",
|
||||
"HAS_ENABLED_PUSH": "You have enabled push for this browser.",
|
||||
"REQUEST_PUSH": "Enable push notifications"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "Home",
|
||||
"AGENTS": "Agents",
|
||||
"INBOXES": "Inboxes",
|
||||
"NOTIFICATIONS": "Notifications",
|
||||
"CANNED_RESPONSES": "Canned Responses",
|
||||
"INTEGRATIONS": "Integrations",
|
||||
"ACCOUNT_SETTINGS": "Account Settings",
|
||||
"LABELS": "Labels"
|
||||
"LABELS": "Labels",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "New Account",
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Recibido por email",
|
||||
"VIEW_TWEET_IN_TWITTER": "Ver tweet en Twitter",
|
||||
"REPLY_TO_TWEET": "Responder a este tweet"
|
||||
"REPLY_TO_TWEET": "Responder a este tweet",
|
||||
"NO_MESSAGES": "No Messages"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "Eliminar selección",
|
||||
"DOWNLOAD": "Descargar",
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"NO_RESPONSE": "No response",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resolver",
|
||||
"REOPEN_ACTION": "Reabrir",
|
||||
@ -44,7 +45,8 @@
|
||||
"TWEET": "Tweet",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Nota privada: solo visible para ti y tu equipo",
|
||||
"CHANGE_STATUS": "Estado de la conversación cambiado",
|
||||
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"SELECT_ONE": "Select one"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Notificaciones",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"TABLE_HEADER": [
|
||||
"Nombre",
|
||||
"Número telefónico",
|
||||
"Conversaciones",
|
||||
"Last Contacted"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message",
|
||||
"conversation_mention": "Mention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "Actualiza tus preferencias de notificación por correo electrónico aquí",
|
||||
"CONVERSATION_ASSIGNMENT": "Enviar notificaciones por correo electrónico cuando se me ha asignado una conversación",
|
||||
"CONVERSATION_CREATION": "Enviar notificaciones por correo electrónico cuando se crea una nueva conversación",
|
||||
"CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Envirar notificaciones por correo electrónico cuando un nuevo mensaje es creado en una conversación asignada"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "Actualize sus preferencias de notificaciones push aquí",
|
||||
"CONVERSATION_ASSIGNMENT": "Enviar notificaciones push cuando se me ha asignado una conversación",
|
||||
"CONVERSATION_CREATION": "Enviar notificaciones push cuando se crea una nueva conversación",
|
||||
"CONVERSATION_MENTION": "Send push notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Enviar notificaciones push cuando un nuevo mensaja es creadao en una conversación asignada",
|
||||
"HAS_ENABLED_PUSH": "Has habilitado notificaciones push para este navegador.",
|
||||
"REQUEST_PUSH": "Habilitar notificaciones push"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "Inicio",
|
||||
"AGENTS": "Agentes",
|
||||
"INBOXES": "Entradas",
|
||||
"NOTIFICATIONS": "Notificaciones",
|
||||
"CANNED_RESPONSES": "Respuestas predefinidas",
|
||||
"INTEGRATIONS": "Integraciones",
|
||||
"ACCOUNT_SETTINGS": "Configuración de la cuenta",
|
||||
"LABELS": "Etiquetas"
|
||||
"LABELS": "Etiquetas",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "Nueva cuenta",
|
||||
|
||||
@ -14,9 +14,9 @@
|
||||
"ERROR": "Full name is too short"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Work email",
|
||||
"PLACEHOLDER": "Enter your work email address. eg: bruce@wayne.enterprises",
|
||||
"ERROR": "Email address is invalid"
|
||||
"LABEL": "Email de trabajo",
|
||||
"PLACEHOLDER": "Introduzca su dirección de correo electrónico de trabajo. Ejemplo: bruce@wayne.enterprises",
|
||||
"ERROR": "Dirección de correo no válida"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Contraseña",
|
||||
@ -33,6 +33,6 @@
|
||||
"ERROR_MESSAGE": "No se pudo conectar al servidor Woot, por favor inténtalo de nuevo más tarde"
|
||||
},
|
||||
"SUBMIT": "Enviar",
|
||||
"HAVE_AN_ACCOUNT": "Already have an account?"
|
||||
"HAVE_AN_ACCOUNT": "¿Ya tienes una cuenta?"
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "از طریق ایمیل دریافت شد",
|
||||
"VIEW_TWEET_IN_TWITTER": "مشاهده توییت در توییتر",
|
||||
"REPLY_TO_TWEET": "پاسخ به این توییت"
|
||||
"REPLY_TO_TWEET": "پاسخ به این توییت",
|
||||
"NO_MESSAGES": "هیچ پیامی وجود ندارد"
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,9 +34,9 @@
|
||||
"NO_AVAILABLE_LABELS": "هیچ برچسبی به این گفتگو اضافه نشده است."
|
||||
},
|
||||
"MUTE_CONTACT": "بیصدا کردن گفتگو",
|
||||
"UNMUTE_CONTACT": "Unmute Conversation",
|
||||
"UNMUTE_CONTACT": "خارج کردن از حالت بی صدا",
|
||||
"MUTED_SUCCESS": "این گفتگو به مدت ۶ ساعت بیصدا است",
|
||||
"UNMUTED_SUCCESS": "This conversation is unmuted",
|
||||
"UNMUTED_SUCCESS": "این گفتگو از حالت بی صدا خارج شده است",
|
||||
"SEND_TRANSCRIPT": "ارسال متن",
|
||||
"EDIT_LABEL": "ویرایش"
|
||||
},
|
||||
@ -108,7 +108,7 @@
|
||||
"نام",
|
||||
"شماره تلفن",
|
||||
"گفتگوها",
|
||||
"Last Contacted"
|
||||
"آخرین تماس"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"CONVERSATION": {
|
||||
"404": "لطفا یک گفتگو را از پنجره سمت چپ انتخاب کنید",
|
||||
"404": "لطفا یک گفتگو را از پنجره گفتگوها انتخاب کنید",
|
||||
"NO_MESSAGE_1": "اوه اوه! به نظر میرسد هیچ پیامی از طرف مشتری در صندوق ورودی شما وجود ندارد.",
|
||||
"NO_MESSAGE_2": " برای ارسال پیام به صفحه خود بروید!",
|
||||
"NO_INBOX_1": "سلام! به نظر میرسد هنوز صندوق ورودی اضافه نکردهاید.",
|
||||
@ -11,7 +11,7 @@
|
||||
"TITLE": "جستجو پیامها",
|
||||
"LOADING_MESSAGE": "درحال پردازش داده...",
|
||||
"PLACEHOLDER": "متنی برای جستجو پیام تایپ کنید",
|
||||
"NO_MATCHING_RESULTS": "There are no messages matching the search parameters."
|
||||
"NO_MATCHING_RESULTS": "گفتگویی با مشخصات ورودی یافت نشد."
|
||||
},
|
||||
"UNREAD_MESSAGES": "پیامهای خوانده نشده",
|
||||
"UNREAD_MESSAGE": "پیام خوانده نشده",
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "حذف انتخابشدهها",
|
||||
"DOWNLOAD": "دانلود",
|
||||
"UPLOADING_ATTACHMENTS": "در حال بارگذاری پیوستها...",
|
||||
"NO_RESPONSE": "بدون پاسخ",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "حل شد",
|
||||
"REOPEN_ACTION": "دوباره باز کنید",
|
||||
@ -33,8 +34,8 @@
|
||||
"DETAILS": "جزئیات"
|
||||
},
|
||||
"FOOTER": {
|
||||
"MSG_INPUT": "Shift + enter for new line. Start with '/' to select a Canned Response.",
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter for new line. This will be visible only to Agents"
|
||||
"MSG_INPUT": "برای رفتن به سرخط shift+enter و برای استفاده از پیام های ذخیره شده / را بزنید.",
|
||||
"PRIVATE_MSG_INPUT": "برای رفتن به سرخط shift+enter را بزنید. این پیام فقط به ایجنت ها نمایش داده میشود"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "پاسخ",
|
||||
@ -42,18 +43,19 @@
|
||||
"SEND": "ارسال",
|
||||
"CREATE": "افزودن یادداشت",
|
||||
"TWEET": "توییت",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_FORMAT_ICON": "نمایش ویرایشگر متنی پیشرفته",
|
||||
"TIP_EMOJI_ICON": "انتخاب ایموجی",
|
||||
"TIP_ATTACH_ICON": "ضمیمه فایل",
|
||||
"ENTER_TO_SEND": "برای ارسال Enter را بزنید"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "یادداشت خصوصی: فقط برای شما و تیم شما قابل مشاهده است",
|
||||
"CHANGE_STATUS": "وضعیت گفتگو تغییر کرد",
|
||||
"CHANGE_AGENT": "Conversation Assignee changed",
|
||||
"SENT_BY": "Sent by:",
|
||||
"CHANGE_AGENT": "مسول گفتگو تغییر کرد",
|
||||
"SENT_BY": "ارسال شده توسط:",
|
||||
"ASSIGNMENT": {
|
||||
"SELECT_AGENT": "Select Agent",
|
||||
"SELECT_AGENT": "انتخاب ایجنت",
|
||||
"REMOVE": "حذف",
|
||||
"ASSIGN": "Assign"
|
||||
"ASSIGN": "اختصاص دادن"
|
||||
}
|
||||
},
|
||||
"EMAIL_TRANSCRIPT": {
|
||||
@ -65,7 +67,7 @@
|
||||
"SEND_EMAIL_ERROR": "خطایی پیش آمد. لطفا دوباره امتحان کنید",
|
||||
"FORM": {
|
||||
"SEND_TO_CONTACT": "متن گفتگو را برای مشتری ارسال کنید",
|
||||
"SEND_TO_AGENT": "Send the transcript to the assigned agent",
|
||||
"SEND_TO_AGENT": "متن گفتگو را به ایجنت اختصاص یافته ارسال کنید",
|
||||
"SEND_TO_OTHER_EMAIL_ADDRESS": "متن گفتگو را به آدرس ایمیل دیگری ارسال کنید",
|
||||
"EMAIL": {
|
||||
"PLACEHOLDER": "یک آدرس ایمیل وارد کنید",
|
||||
|
||||
@ -34,22 +34,42 @@
|
||||
"ERROR": ""
|
||||
},
|
||||
"AUTO_RESOLVE_DURATION": {
|
||||
"LABEL": "Number of days after a ticket should auto resolve if there is no activity",
|
||||
"LABEL": "تعداد روزهایی که اگر فعالیتی وجود نداشته باشد، گفتگو به صورت خودکار بسته شود",
|
||||
"PLACEHOLDER": "30",
|
||||
"ERROR": "Please enter a valid auto resolve duration (minimum 1 day)"
|
||||
"ERROR": "لطفاً یک مدت زمان حل خودکار معتبر (حداقل 1 روز) وارد کنید"
|
||||
},
|
||||
"FEATURES": {
|
||||
"INBOUND_EMAIL_ENABLED": "Conversation continuity with emails is enabled for your account.",
|
||||
"INBOUND_EMAIL_ENABLED": "تداوم مکالمه با ایمیل برای حساب شما فعال است.",
|
||||
"CUSTOM_EMAIL_DOMAIN_ENABLED": "اکنون میتوانید ایمیلها را در دامنه سفارشی خود دریافت کنید."
|
||||
}
|
||||
},
|
||||
"UPDATE_CHATWOOT": "An update %{latestChatwootVersion} for Chatwoot is available. Please update your instance."
|
||||
"UPDATE_CHATWOOT": "به روزرسانی%{latestChatwootVersion} برای Chatwoot در دسترس است. لطفا نمونه خود را به روز کنید."
|
||||
},
|
||||
"FORMS": {
|
||||
"MULTISELECT": {
|
||||
"ENTER_TO_SELECT": "Press enter to select",
|
||||
"ENTER_TO_REMOVE": "Press enter to remove",
|
||||
"SELECT_ONE": "Select one"
|
||||
"ENTER_TO_SELECT": "برای انتخاب Enter را فشار دهید",
|
||||
"ENTER_TO_REMOVE": "برای حذف دکمه enter را فشار دهید",
|
||||
"SELECT_ONE": "یکی را انتخاب کن"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "اعلان ها",
|
||||
"MARK_ALL_DONE": "همه را به انجام شده تغییر بده",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "در حال بارگیری اعلان ها...",
|
||||
"404": "اعلانی وجود ندارد",
|
||||
"TABLE_HEADER": [
|
||||
"نام",
|
||||
"شماره تلفن",
|
||||
"گفتگوها",
|
||||
"آخرین تماس"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "گفتگوی جدید",
|
||||
"conversation_assignment": "مکالمه اختصاص داده شد",
|
||||
"assigned_conversation_new_message": "پیام جدید",
|
||||
"conversation_mention": "اشاره"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,10 +75,10 @@
|
||||
},
|
||||
"REPLY_TIME": {
|
||||
"TITLE": "تنظیم زمان پاسخ",
|
||||
"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": "در چند دقیقه",
|
||||
"IN_A_FEW_HOURS": "در چند ساعت",
|
||||
"IN_A_DAY": "در یک روز",
|
||||
"HELP_TEXT": "این زمان پاسخ در ویجت چت زنده نمایش داده می شود"
|
||||
},
|
||||
"WIDGET_COLOR": {
|
||||
"LABEL": "رنگ ویجت",
|
||||
@ -124,7 +124,7 @@
|
||||
},
|
||||
"API_CHANNEL": {
|
||||
"TITLE": "کانال API",
|
||||
"DESC": "Integrate with API channel and start supporting your customers.",
|
||||
"DESC": "با کانال API ادغام شده و از مشتریان خود پشتیبانی کنید.",
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "عنوان کانال",
|
||||
"PLACEHOLDER": "لطفا اسم یک کانال را وارد کنید",
|
||||
@ -132,12 +132,12 @@
|
||||
},
|
||||
"WEBHOOK_URL": {
|
||||
"LABEL": "آدرس URL وب هوک",
|
||||
"SUBTITLE": "Configure the URL where you want to recieve callbacks on events.",
|
||||
"SUBTITLE": "آدرس کال بک خود را جهت دریافت اطلاعات رویداد های وارد کنید.",
|
||||
"PLACEHOLDER": "آدرس URL وب هوک"
|
||||
},
|
||||
"SUBMIT_BUTTON": "ایجاد کانال API",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the api channel"
|
||||
"ERROR_MESSAGE": "کانال API ذخیره نشد"
|
||||
}
|
||||
},
|
||||
"EMAIL_CHANNEL": {
|
||||
@ -150,14 +150,14 @@
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "ایمیل",
|
||||
"SUBTITLE": "Email where your customers sends you support tickets",
|
||||
"SUBTITLE": "آدرس ایمیلی که مشتریان درخواست پشتیبانی را برای شما ارسال کنند",
|
||||
"PLACEHOLDER": "ایمیل"
|
||||
},
|
||||
"SUBMIT_BUTTON": "ایجاد کانال ایمیل",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the email channel"
|
||||
"ERROR_MESSAGE": "کانال ایمیل ذخیره نشد"
|
||||
},
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
"FINISH_MESSAGE": "ایمیل های خود را به این آدرس ها فوروارد کنید."
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "کانالها",
|
||||
@ -166,8 +166,8 @@
|
||||
"AGENTS": {
|
||||
"TITLE": "اپراتورها",
|
||||
"DESC": "در اینجا میتوانید اپراتورها را به صندوق ورودی خود اختصاص دهید. توجه داشته باشید که فقط اپراتورهایی که در اینجا معرفی شده باشند میتوانند به پیامهای این صندوق پاسخ بدهند.دیگر اپراتورها نخواهند توانست پیامهای این صندوق را ببینید یا به آنها پاسخی بدهند. <br> <b>پانویس:</b>به عنوان مدیر اگر میخواهید به همه صندوقهای ورودی دسترسی داشته باشید میبایست خود را به عنوان اپراتور به همه صندوقها اضافه کنید.",
|
||||
"VALIDATION_ERROR": "Add atleast one agent to your new Inbox",
|
||||
"PICK_AGENTS": "Pick agents for the inbox"
|
||||
"VALIDATION_ERROR": "حداقل یک ایجنت به اینباکس جدید اضافه کنید",
|
||||
"PICK_AGENTS": "انتخاب ایجنت ها برای این صندوق ورودی"
|
||||
},
|
||||
"DETAILS": {
|
||||
"TITLE": "جزئیات صندوق ورودی",
|
||||
@ -229,8 +229,8 @@
|
||||
"SETTINGS": "تنظیمات",
|
||||
"FEATURES": {
|
||||
"LABEL": "امکانات",
|
||||
"DISPLAY_FILE_PICKER": "Display file picker on the widget",
|
||||
"DISPLAY_EMOJI_PICKER": "Display emoji picker on the widget"
|
||||
"DISPLAY_FILE_PICKER": "نمایش گزینه ضمیه فایل در ویجت",
|
||||
"DISPLAY_EMOJI_PICKER": "نمایش گزینه انتخاب ایموجی در ویجت"
|
||||
},
|
||||
"SETTINGS_POPUP": {
|
||||
"MESSENGER_HEADING": "اسکریپت ویجت",
|
||||
@ -242,13 +242,13 @@
|
||||
"INBOX_UPDATE_TITLE": "تنظیمات صندوق ورودی",
|
||||
"INBOX_UPDATE_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."
|
||||
"HMAC_VERIFICATION": "اعتبار سنجی کاربر",
|
||||
"HMAC_DESCRIPTION": "برای اعتبار سنجی کاربران با استفاده از این کلید یک هش HMAC sha256 تولید کنید."
|
||||
},
|
||||
"FACEBOOK_REAUTHORIZE": {
|
||||
"TITLE": "احراز هویت مجدد",
|
||||
"SUBTITLE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
|
||||
"MESSAGE_SUCCESS": "Reconnection successful",
|
||||
"SUBTITLE": "اتصال فیس بوک شما منقضی شده است ، لطفاً برای ادامه خدمات دوباره صفحه فیس بوک خود را متصل کنید",
|
||||
"MESSAGE_SUCCESS": "اتصال مجدد موفقیت آمیز بود",
|
||||
"MESSAGE_ERROR": "خطایی پیش آمد. لطفا دوباره امتحان کنید"
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
"HEADER_BTN_TXT": "افزودن برچسب",
|
||||
"LOADING": "درحال گرفتن برچسبها",
|
||||
"SEARCH_404": "هیچ آیتمی با این مشخصات یافت نشد",
|
||||
"SIDEBAR_TXT": "<p><b>Labels</b> <p>Labels help you to categorize conversations and prioritize them. You can assign label to a conversation from the sidepanel. <br /><br />Labels are tied to the account and can be used to create custom workflows in your organization. You can assign custom color to a label, it makes it easier to identify the label. You will be able to display the label on the sidebar to filter the conversations easily.</p>",
|
||||
"SIDEBAR_TXT": "<p>\n <b> برچسب ها</b>\n<p> برچسب ها به شما در دسته بندی مکالمات و اولویت بندی آنها کمک می کند. می توانید برچسب را به یک مکالمه از نوار کناری اختصاص دهید.\n <br/>\n <br/>\n برچسب ها به حساب گره خورده اند و می توانند برای ایجاد گردش کار سفارشی در سازمان شما استفاده شوند. می توانید رنگ سفارشی را به برچسب اختصاص دهید ، شناسایی برچسب آسان تر می شود. شما می توانید برچسب را در نوار کناری نمایش دهید تا مکالمه ها به راحتی فیلتر شود.\n</p>",
|
||||
"LIST": {
|
||||
"404": "هیچ برچسبی در این حسابکاربری وجود ندارد.",
|
||||
"TITLE": "مدیریت برچسبها",
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"HEADER": "گزارشات",
|
||||
"LOADING_CHART": "در حال دریافت اطلاعات...",
|
||||
"NO_ENOUGH_DATA": "متاسفانه اطلاعات کافی دریافت نشد، لطفا بعدا دوباره امتحان کنید",
|
||||
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
|
||||
"DOWNLOAD_AGENT_REPORTS": "دانلود گزارش ایجنت",
|
||||
"METRICS": {
|
||||
"CONVERSATIONS": {
|
||||
"NAME": "گفتگوها",
|
||||
|
||||
@ -27,7 +27,8 @@
|
||||
"NOTE": "اینجا میتوانید تنظیمات اعلامیههایی که به ایمیل ارسال میشود تغییر دهید",
|
||||
"CONVERSATION_ASSIGNMENT": "هر وقت گفتگویی به من اختصاص داده شد، ایمیل بفرست",
|
||||
"CONVERSATION_CREATION": "هر وقت گفتگوی جدیدی شروع شد برای من ایمیل بفرست",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation"
|
||||
"CONVERSATION_MENTION": "هنگامی که از شما در گفتگو نام برده میشود، از طریق ایمیل آگاهسازی ارسال کن",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "با ایجاد پیام جدید در مکالمه اختصاصی ، اعلان های ایمیل را ارسال کنید"
|
||||
},
|
||||
"API": {
|
||||
"UPDATE_SUCCESS": "تغییرات تنظیمات اعلامیهها با موفقیت ثبت شد",
|
||||
@ -38,7 +39,8 @@
|
||||
"NOTE": "اینجا میتوانید تنظیمات پوش نوتیفیکیشن را تغییر دهید",
|
||||
"CONVERSATION_ASSIGNMENT": "هر وقت گفتگویی به من اختصاص داده شد، برای من پوش نوتیفیکیشن بفرست",
|
||||
"CONVERSATION_CREATION": "هر وقت گفتگوی جدیدی شروع شد برای من پوش نوتیفیکیشن بفرست",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation",
|
||||
"CONVERSATION_MENTION": "هنگامی که از شما در مکالمه نام برده می شود پوش نوتیفیکیشن ارسال کنید",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "وقتی پیام جدیدی در مکالمه اختصاص داده شده ایجاد می شود ، پوش نوتیفیکیشن را ارسال کنید",
|
||||
"HAS_ENABLED_PUSH": "در این مرورگر پوش نوتیفیکیشن را فعال کردهاید",
|
||||
"REQUEST_PUSH": "فعال کردن پوش نوتیفیکیشن"
|
||||
},
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "صفحه اصلی",
|
||||
"AGENTS": "اپراتورها",
|
||||
"INBOXES": "صندوقهای ورودی",
|
||||
"NOTIFICATIONS": "اعلان ها",
|
||||
"CANNED_RESPONSES": "پاسخهای آماده",
|
||||
"INTEGRATIONS": "برنامههای تلفیق شده",
|
||||
"ACCOUNT_SETTINGS": "تنظیمات حساب",
|
||||
"LABELS": "برچسبها"
|
||||
"LABELS": "برچسبها",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "حسابکاربری جدید",
|
||||
|
||||
@ -2,11 +2,11 @@
|
||||
"REGISTER": {
|
||||
"TRY_WOOT": "ثبت حساب",
|
||||
"TITLE": "ثبت نام",
|
||||
"TERMS_ACCEPT": "با ثبت نام، اعلام میدارید که <a href=\"https://www.chatwoot.com/terms\">قوانین</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">شرایط استفاده</a> از ما را تایید کرده و میپذیرید.",
|
||||
"TERMS_ACCEPT": "با ثبت نام، اعلام میدارید که <a href=\"https://www.chatwoot.com/terms\">قوانین</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">شرایط استفاده</a> از این نرم افزار را تایید کرده و میپذیرید",
|
||||
"ACCOUNT_NAME": {
|
||||
"LABEL": "عنوان حساب",
|
||||
"PLACEHOLDER": "نام حسابکاربری را وارد کنید. به عنوان مثال: شرکت وین",
|
||||
"ERROR": "نام حسابکاربری خیلی کوتاه است"
|
||||
"PLACEHOLDER": "نام حساب کاربری را وارد کنید. به عنوان مثال: شرکت وین",
|
||||
"ERROR": "نام حساب کاربری خیلی کوتاه است"
|
||||
},
|
||||
"FULL_NAME": {
|
||||
"LABEL": "نام کامل",
|
||||
@ -33,6 +33,6 @@
|
||||
"ERROR_MESSAGE": "ارتباط با سرور برقرار نشد، لطفا بعدا امتحان کنید"
|
||||
},
|
||||
"SUBMIT": "ثبت",
|
||||
"HAVE_AN_ACCOUNT": "از قبل حسابکاربری دارید؟"
|
||||
"HAVE_AN_ACCOUNT": "از قبل حساب کاربری دارید؟"
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Vastaanotettu sähköpostitse",
|
||||
"VIEW_TWEET_IN_TWITTER": "Näytä twiitti Twitterissä",
|
||||
"REPLY_TO_TWEET": "Vastaa tähän twiittiin"
|
||||
"REPLY_TO_TWEET": "Vastaa tähän twiittiin",
|
||||
"NO_MESSAGES": "No Messages"
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@
|
||||
"REMOVE_SELECTION": "Poista valinnat",
|
||||
"DOWNLOAD": "Lataa",
|
||||
"UPLOADING_ATTACHMENTS": "Ladataan liitteitä...",
|
||||
"NO_RESPONSE": "No response",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Ratkaise",
|
||||
"REOPEN_ACTION": "Uudelleenavaa",
|
||||
@ -44,7 +45,8 @@
|
||||
"TWEET": "Twiittaa",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files"
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"ENTER_TO_SEND": "Enter to send"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Yksityinen huomautus: Näkyy vain sinulle ja tiimillesi",
|
||||
"CHANGE_STATUS": "Keskustelun tila muutettu",
|
||||
|
||||
@ -51,5 +51,25 @@
|
||||
"ENTER_TO_REMOVE": "Paina enter poistaaksesi",
|
||||
"SELECT_ONE": "Valitse yksi"
|
||||
}
|
||||
},
|
||||
"NOTIFICATIONS_PAGE": {
|
||||
"HEADER": "Ilmoitukset",
|
||||
"MARK_ALL_DONE": "Mark All Done",
|
||||
"LIST": {
|
||||
"LOADING_MESSAGE": "Loading notifications...",
|
||||
"404": "No Notifications",
|
||||
"TABLE_HEADER": [
|
||||
"Nimi",
|
||||
"Puhelinnumero",
|
||||
"Keskustelut",
|
||||
"Viimeksi otettu yhteyttä"
|
||||
]
|
||||
},
|
||||
"TYPE_LABEL": {
|
||||
"conversation_creation": "New conversation",
|
||||
"conversation_assignment": "Conversation Assigned",
|
||||
"assigned_conversation_new_message": "New Message",
|
||||
"conversation_mention": "Mention"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"NOTE": "Päivitä sähköposti-ilmoitusasetukset täällä",
|
||||
"CONVERSATION_ASSIGNMENT": "Lähetä sähköposti-ilmoitus, kun keskustelu on määritetty minulle",
|
||||
"CONVERSATION_CREATION": "Lähetä sähköposti-ilmoitus, kun keskustelu on määritetty minulle",
|
||||
"CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Lähetä sähköposti-ilmoitus, kun uusi viesti saapuu minulle osoitettuun keskusteluun"
|
||||
},
|
||||
"API": {
|
||||
@ -38,6 +39,7 @@
|
||||
"NOTE": "Päivitä push-ilmoitusasetukset täällä",
|
||||
"CONVERSATION_ASSIGNMENT": "Lähetä push-ilmoitus, kun keskustelu on määritetty minulle",
|
||||
"CONVERSATION_CREATION": "Lähetä push-ilmoitus, kun keskustelu on määritetty minulle",
|
||||
"CONVERSATION_MENTION": "Send push notifications when you are mentioned in a conversation",
|
||||
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Lähetä push-ilmoitus, kun uusi viesti saapuu minulle osoitettuun keskusteluun",
|
||||
"HAS_ENABLED_PUSH": "Tämän selaimen push-ilmoitukset on otettu käyttöön.",
|
||||
"REQUEST_PUSH": "Ota push-ilmoitukset käyttöön"
|
||||
@ -118,10 +120,12 @@
|
||||
"HOME": "Koti",
|
||||
"AGENTS": "Edustajat",
|
||||
"INBOXES": "Kansiot",
|
||||
"NOTIFICATIONS": "Ilmoitukset",
|
||||
"CANNED_RESPONSES": "Tallennetut vastaukset",
|
||||
"INTEGRATIONS": "Integraatiot",
|
||||
"ACCOUNT_SETTINGS": "Tilin asetukset",
|
||||
"LABELS": "Tunnisteet"
|
||||
"LABELS": "Tunnisteet",
|
||||
"TEAMS": "Teams"
|
||||
},
|
||||
"CREATE_ACCOUNT": {
|
||||
"NEW_ACCOUNT": "Uusi tili",
|
||||
|
||||
@ -79,6 +79,7 @@
|
||||
},
|
||||
"RECEIVED_VIA_EMAIL": "Reçu par courriel",
|
||||
"VIEW_TWEET_IN_TWITTER": "Voir le tweet sur Twitter",
|
||||
"REPLY_TO_TWEET": "Répondre à ce tweet"
|
||||
"REPLY_TO_TWEET": "Répondre à ce tweet",
|
||||
"NO_MESSAGES": "No Messages"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user