Merge branch 'release/1.13.0'

This commit is contained in:
Sojan 2021-02-16 19:54:24 +05:30
commit ec1ddc06fd
533 changed files with 8856 additions and 3906 deletions

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View 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

View File

@ -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]
)

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,6 @@
class Twitter::CallbacksController < Twitter::BaseController
include TwitterConcern
def show
return redirect_to twitter_app_redirect_url if permitted_params[:denied]

View File

@ -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

View File

@ -9,6 +9,8 @@ module FileTypeHelper
'image/bmp'
].include?(content_type)
return :audio if content_type.include?('audio/')
:file
end
end

View 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();

View File

@ -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,
},
});
}

View 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();

View File

@ -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');
});
});

View 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');
});
});

View 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');
});
});

View 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();

View File

@ -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;

View File

@ -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;

View File

@ -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();
},

View File

@ -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,

View File

@ -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>

View File

@ -155,7 +155,6 @@ export default {
.wrap {
display: flex;
align-items: center;
}
.label-color--display {

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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);
},
},
};

View File

@ -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) }}

View File

@ -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 {

View File

@ -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 });
},
},
};

View File

@ -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>

View File

@ -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 '';
};

View File

@ -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/>'
);
});
});

View File

@ -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>

View File

@ -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',

View File

@ -79,6 +79,7 @@
},
"RECEIVED_VIA_EMAIL": "تم تلقيه عبر البريد الإلكتروني",
"VIEW_TWEET_IN_TWITTER": "عرض التغريدة في تويتر",
"REPLY_TO_TWEET": "الرد على هذه التغريدة"
"REPLY_TO_TWEET": "الرد على هذه التغريدة",
"NO_MESSAGES": "No Messages"
}
}

View File

@ -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": "تم تغيير حالة المحادثة",

View File

@ -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"
}
}
}

View File

@ -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": "حساب جديد",

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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ó"
}
}
}

View File

@ -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": {

View File

@ -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 massigni 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",

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -79,6 +79,7 @@
},
"RECEIVED_VIA_EMAIL": "Παραλήφθηκε από email",
"VIEW_TWEET_IN_TWITTER": "Προβολή του tweet στο Twitter",
"REPLY_TO_TWEET": "Απάντηση στο tweet"
"REPLY_TO_TWEET": "Απάντηση στο tweet",
"NO_MESSAGES": "Κανένα Μήνυμα"
}
}

View File

@ -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": "Η κατάσταση της συνομιλίας άλλαξε",

View File

@ -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": "Αναφορά"
}
}
}

View File

@ -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": "Νέος Λογαριασμός",

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -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",

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -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?"
}
}

View File

@ -79,6 +79,7 @@
},
"RECEIVED_VIA_EMAIL": "از طریق ایمیل دریافت شد",
"VIEW_TWEET_IN_TWITTER": "مشاهده توییت در توییتر",
"REPLY_TO_TWEET": "پاسخ به این توییت"
"REPLY_TO_TWEET": "پاسخ به این توییت",
"NO_MESSAGES": "هیچ پیامی وجود ندارد"
}
}

View File

@ -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"
"آخرین تماس"
]
}
}

View File

@ -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": "یک آدرس ایمیل وارد کنید",

View File

@ -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": "اشاره"
}
}
}

View File

@ -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": "خطایی پیش آمد. لطفا دوباره امتحان کنید"
}
}

View File

@ -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": "مدیریت برچسب‌ها",

View File

@ -3,7 +3,7 @@
"HEADER": "گزارشات",
"LOADING_CHART": "در حال دریافت اطلاعات...",
"NO_ENOUGH_DATA": "متاسفانه اطلاعات کافی دریافت نشد، لطفا بعدا دوباره امتحان کنید",
"DOWNLOAD_AGENT_REPORTS": "Download agent reports",
"DOWNLOAD_AGENT_REPORTS": "دانلود گزارش ایجنت",
"METRICS": {
"CONVERSATIONS": {
"NAME": "گفتگوها",

View File

@ -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": "حساب‌کاربری جدید",

View File

@ -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": "از قبل حساب ‌کاربری دارید؟"
}
}

View File

@ -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"
}
}

View File

@ -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",

View File

@ -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"
}
}
}

View File

@ -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",

View File

@ -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