Merge branch 'develop' into codex/linear-mention-cw-5548-cannot-create-a-linear-ticket
This commit is contained in:
@@ -274,3 +274,5 @@ AZURE_APP_SECRET=
|
||||
# Set to true if you want to remove stale contact inboxes
|
||||
# contact_inboxes with no conversation older than 90 days will be removed
|
||||
# REMOVE_STALE_CONTACT_INBOX_JOB_STATUS=false
|
||||
|
||||
# REDIS_ALFRED_SIZE=10
|
||||
|
||||
@@ -47,8 +47,14 @@
|
||||
- Avoid writing specs unless explicitly asked
|
||||
- Remove dead/unreachable/unused code
|
||||
- Don’t write multiple versions or backups for the same logic — pick the best approach and implement it
|
||||
- Don't reference Claude in commit messages
|
||||
- Prefer `with_modified_env` (from spec helpers) over stubbing `ENV` directly in specs
|
||||
- Specs in parallel/reloading environments: prefer comparing `error.class.name` over constant class equality when asserting raised errors
|
||||
|
||||
## Commit Messages
|
||||
|
||||
- Prefer Conventional Commits: `type(scope): subject` (scope optional)
|
||||
- Example: `feat(auth): add user authentication`
|
||||
- Don't reference Claude in commit messages
|
||||
|
||||
## Project-Specific
|
||||
|
||||
|
||||
+16
-13
@@ -140,24 +140,27 @@ GEM
|
||||
actionmailbox (>= 7.1.0)
|
||||
aws-sdk-s3 (~> 1, >= 1.123.0)
|
||||
aws-sdk-sns (~> 1, >= 1.61.0)
|
||||
aws-eventstream (1.2.0)
|
||||
aws-partitions (1.760.0)
|
||||
aws-sdk-core (3.188.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1198.0)
|
||||
aws-sdk-core (3.240.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.64.0)
|
||||
aws-sdk-core (~> 3, >= 3.165.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sdk-s3 (1.126.0)
|
||||
aws-sdk-core (~> 3, >= 3.174.0)
|
||||
logger
|
||||
aws-sdk-kms (1.118.0)
|
||||
aws-sdk-core (~> 3, >= 3.239.1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.208.0)
|
||||
aws-sdk-core (~> 3, >= 3.234.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.4)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-sns (1.70.0)
|
||||
aws-sdk-core (~> 3, >= 3.188.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.5.2)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
barnes (0.0.9)
|
||||
multi_json (~> 1)
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
4.8.0
|
||||
4.9.1
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseController
|
||||
include AttachmentConcern
|
||||
|
||||
before_action :check_authorization
|
||||
before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone]
|
||||
|
||||
@@ -9,25 +11,32 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||
def show; end
|
||||
|
||||
def create
|
||||
blobs, actions, error = validate_and_prepare_attachments(params[:actions])
|
||||
return render_could_not_create_error(error) if error
|
||||
|
||||
@automation_rule = Current.account.automation_rules.new(automation_rules_permit)
|
||||
@automation_rule.actions = params[:actions]
|
||||
@automation_rule.actions = actions
|
||||
@automation_rule.conditions = params[:conditions]
|
||||
|
||||
render json: { error: @automation_rule.errors.messages }, status: :unprocessable_entity and return unless @automation_rule.valid?
|
||||
return render_could_not_create_error(@automation_rule.errors.messages) unless @automation_rule.valid?
|
||||
|
||||
@automation_rule.save!
|
||||
process_attachments
|
||||
@automation_rule
|
||||
blobs.each { |blob| @automation_rule.files.attach(blob) }
|
||||
end
|
||||
|
||||
def update
|
||||
ActiveRecord::Base.transaction do
|
||||
automation_rule_update
|
||||
process_attachments
|
||||
blobs, actions, error = validate_and_prepare_attachments(params[:actions], @automation_rule)
|
||||
return render_could_not_create_error(error) if error
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@automation_rule.assign_attributes(automation_rules_permit)
|
||||
@automation_rule.actions = actions if params[:actions]
|
||||
@automation_rule.conditions = params[:conditions] if params[:conditions]
|
||||
@automation_rule.save!
|
||||
blobs.each { |blob| @automation_rule.files.attach(blob) }
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @automation_rule.errors.messages }.to_json, status: :unprocessable_entity
|
||||
render_could_not_create_error(@automation_rule.errors.messages)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,29 +52,11 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont
|
||||
@automation_rule = new_rule
|
||||
end
|
||||
|
||||
def process_attachments
|
||||
actions = @automation_rule.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||
return if actions.blank?
|
||||
|
||||
actions.each do |action|
|
||||
blob_id = action['action_params']
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
@automation_rule.files.attach(blob)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def automation_rule_update
|
||||
@automation_rule.update!(automation_rules_permit)
|
||||
@automation_rule.actions = params[:actions] if params[:actions]
|
||||
@automation_rule.conditions = params[:conditions] if params[:conditions]
|
||||
@automation_rule.save!
|
||||
end
|
||||
|
||||
def automation_rules_permit
|
||||
params.permit(
|
||||
:name, :description, :event_name, :account_id, :active,
|
||||
:name, :description, :event_name, :active,
|
||||
conditions: [:attribute_key, :filter_operator, :query_operator, :custom_attribute_type, { values: [] }],
|
||||
actions: [:action_name, { action_params: [] }]
|
||||
)
|
||||
|
||||
@@ -64,7 +64,7 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::V1::Accounts:
|
||||
|
||||
def permitted_params
|
||||
params.require(:twilio_channel).permit(
|
||||
:account_id, :messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium, :api_key_sid
|
||||
:messaging_service_sid, :phone_number, :account_sid, :auth_token, :name, :medium, :api_key_sid
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
include AttachmentConcern
|
||||
|
||||
before_action :fetch_macro, only: [:show, :update, :destroy, :execute]
|
||||
before_action :check_authorization, only: [:show, :update, :destroy, :execute]
|
||||
|
||||
@@ -11,26 +13,32 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def create
|
||||
blobs, actions, error = validate_and_prepare_attachments(params[:actions])
|
||||
return render_could_not_create_error(error) if error
|
||||
|
||||
@macro = Current.account.macros.new(macros_with_user.merge(created_by_id: current_user.id))
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
@macro.actions = params[:actions]
|
||||
@macro.actions = actions
|
||||
|
||||
render json: { error: @macro.errors.messages }, status: :unprocessable_entity and return unless @macro.valid?
|
||||
return render_could_not_create_error(@macro.errors.messages) unless @macro.valid?
|
||||
|
||||
@macro.save!
|
||||
process_attachments
|
||||
@macro
|
||||
blobs.each { |blob| @macro.files.attach(blob) }
|
||||
end
|
||||
|
||||
def update
|
||||
blobs, actions, error = validate_and_prepare_attachments(params[:actions], @macro)
|
||||
return render_could_not_create_error(error) if error
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@macro.update!(macros_with_user)
|
||||
@macro.assign_attributes(macros_with_user)
|
||||
@macro.set_visibility(current_user, permitted_params)
|
||||
process_attachments
|
||||
@macro.actions = actions if params[:actions]
|
||||
@macro.save!
|
||||
blobs.each { |blob| @macro.files.attach(blob) }
|
||||
rescue StandardError => e
|
||||
Rails.logger.error e
|
||||
render json: { error: @macro.errors.messages }.to_json, status: :unprocessable_entity
|
||||
render_could_not_create_error(@macro.errors.messages)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,20 +55,9 @@ class Api::V1::Accounts::MacrosController < Api::V1::Accounts::BaseController
|
||||
|
||||
private
|
||||
|
||||
def process_attachments
|
||||
actions = @macro.actions.filter_map { |k, _v| k if k['action_name'] == 'send_attachment' }
|
||||
return if actions.blank?
|
||||
|
||||
actions.each do |action|
|
||||
blob_id = action['action_params']
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
@macro.files.attach(blob)
|
||||
end
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(
|
||||
:name, :account_id, :visibility,
|
||||
:name, :visibility,
|
||||
actions: [:action_name, { action_params: [] }]
|
||||
)
|
||||
end
|
||||
|
||||
@@ -62,7 +62,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def process_attached_logo
|
||||
blob_id = params[:blob_id]
|
||||
blob = ActiveStorage::Blob.find_by(id: blob_id)
|
||||
blob = ActiveStorage::Blob.find_signed(blob_id)
|
||||
@portal.logo.attach(blob)
|
||||
end
|
||||
|
||||
@@ -78,7 +78,7 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
|
||||
|
||||
def portal_params
|
||||
params.require(:portal).permit(
|
||||
:id, :account_id, :color, :custom_domain, :header_text, :homepage_link,
|
||||
:id, :color, :custom_domain, :header_text, :homepage_link,
|
||||
:name, :page_title, :slug, :archived, { config: [:default_locale, { allowed_locales: [] }] }
|
||||
)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
class Api::V1::Accounts::Tiktok::AuthorizationsController < Api::V1::Accounts::OauthAuthorizationController
|
||||
include Tiktok::IntegrationHelper
|
||||
|
||||
def create
|
||||
redirect_url = Tiktok::AuthClient.authorize_url(
|
||||
state: generate_tiktok_token(Current.account.id)
|
||||
)
|
||||
|
||||
if redirect_url
|
||||
render json: { success: true, url: redirect_url }
|
||||
else
|
||||
render json: { success: false }, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -59,7 +59,7 @@ class Api::V1::Accounts::UploadController < Api::V1::Accounts::BaseController
|
||||
end
|
||||
|
||||
def render_success(file_blob)
|
||||
render json: { file_url: url_for(file_blob), blob_key: file_blob.key, blob_id: file_blob.id }
|
||||
render json: { file_url: url_for(file_blob), blob_id: file_blob.signed_id }
|
||||
end
|
||||
|
||||
def render_error(message, status)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
module AttachmentConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def validate_and_prepare_attachments(actions, record = nil)
|
||||
blobs = []
|
||||
return [blobs, actions, nil] if actions.blank?
|
||||
|
||||
sanitized = actions.map do |action|
|
||||
next action unless action[:action_name] == 'send_attachment'
|
||||
|
||||
result = process_attachment_action(action, record, blobs)
|
||||
return [nil, nil, I18n.t('errors.attachments.invalid')] unless result
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
[blobs, sanitized, nil]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_attachment_action(action, record, blobs)
|
||||
blob_id = action[:action_params].first
|
||||
blob = ActiveStorage::Blob.find_signed(blob_id.to_s)
|
||||
|
||||
return action.merge(action_params: [blob.id]).tap { blobs << blob } if blob.present?
|
||||
return action if blob_already_attached?(record, blob_id)
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def blob_already_attached?(record, blob_id)
|
||||
record&.files&.any? { |f| f.blob_id == blob_id.to_i }
|
||||
end
|
||||
end
|
||||
@@ -73,6 +73,7 @@ class DashboardController < ActionController::Base
|
||||
ENABLE_ACCOUNT_SIGNUP: GlobalConfigService.load('ENABLE_ACCOUNT_SIGNUP', 'false'),
|
||||
FB_APP_ID: GlobalConfigService.load('FB_APP_ID', ''),
|
||||
INSTAGRAM_APP_ID: GlobalConfigService.load('INSTAGRAM_APP_ID', ''),
|
||||
TIKTOK_APP_ID: GlobalConfigService.load('TIKTOK_APP_ID', ''),
|
||||
FACEBOOK_API_VERSION: GlobalConfigService.load('FACEBOOK_API_VERSION', 'v18.0'),
|
||||
WHATSAPP_APP_ID: GlobalConfigService.load('WHATSAPP_APP_ID', ''),
|
||||
WHATSAPP_CONFIGURATION_ID: GlobalConfigService.load('WHATSAPP_CONFIGURATION_ID', ''),
|
||||
|
||||
@@ -46,6 +46,7 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
'linear' => %w[LINEAR_CLIENT_ID LINEAR_CLIENT_SECRET],
|
||||
'slack' => %w[SLACK_CLIENT_ID SLACK_CLIENT_SECRET],
|
||||
'instagram' => %w[INSTAGRAM_APP_ID INSTAGRAM_APP_SECRET INSTAGRAM_VERIFY_TOKEN INSTAGRAM_API_VERSION ENABLE_INSTAGRAM_CHANNEL_HUMAN_AGENT],
|
||||
'tiktok' => %w[TIKTOK_APP_ID TIKTOK_APP_SECRET],
|
||||
'whatsapp_embedded' => %w[WHATSAPP_APP_ID WHATSAPP_APP_SECRET WHATSAPP_CONFIGURATION_ID WHATSAPP_API_VERSION],
|
||||
'notion' => %w[NOTION_CLIENT_ID NOTION_CLIENT_SECRET],
|
||||
'google' => %w[GOOGLE_OAUTH_CLIENT_ID GOOGLE_OAUTH_CLIENT_SECRET GOOGLE_OAUTH_REDIRECT_URI ENABLE_GOOGLE_OAUTH_LOGIN]
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
class Tiktok::CallbacksController < ApplicationController
|
||||
include Tiktok::IntegrationHelper
|
||||
|
||||
def show
|
||||
return handle_authorization_error if params[:error].present?
|
||||
return handle_ungranted_scopes_error unless all_scopes_granted?
|
||||
|
||||
process_successful_authorization
|
||||
rescue StandardError => e
|
||||
handle_error(e)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_scopes_granted?
|
||||
granted_scopes = short_term_access_token[:scope].to_s.split(',')
|
||||
(Tiktok::AuthClient::REQUIRED_SCOPES - granted_scopes).blank?
|
||||
end
|
||||
|
||||
def process_successful_authorization
|
||||
inbox, already_exists = find_or_create_inbox
|
||||
|
||||
if already_exists
|
||||
redirect_to app_tiktok_inbox_settings_url(account_id: account_id, inbox_id: inbox.id)
|
||||
else
|
||||
redirect_to app_tiktok_inbox_agents_url(account_id: account_id, inbox_id: inbox.id)
|
||||
end
|
||||
end
|
||||
|
||||
def handle_error(error)
|
||||
Rails.logger.error("TikTok Channel creation Error: #{error.message}")
|
||||
ChatwootExceptionTracker.new(error).capture_exception
|
||||
|
||||
redirect_to_error_page(error_type: error.class.name, code: 500, error_message: error.message)
|
||||
end
|
||||
|
||||
# Handles the case when a user denies permissions or cancels the authorization flow
|
||||
def handle_authorization_error
|
||||
redirect_to_error_page(
|
||||
error_type: params[:error] || 'access_denied',
|
||||
code: params[:error_code],
|
||||
error_message: params[:error_description] || 'User cancelled the Authorization'
|
||||
)
|
||||
end
|
||||
|
||||
# Handles the case when a user partially accepted the required scopes
|
||||
def handle_ungranted_scopes_error
|
||||
redirect_to_error_page(
|
||||
error_type: 'ungranted_scopes',
|
||||
code: 400,
|
||||
error_message: 'User did not grant all the required scopes'
|
||||
)
|
||||
end
|
||||
|
||||
# Centralized method to redirect to error page with appropriate parameters
|
||||
# This ensures consistent error handling across different error scenarios
|
||||
# Frontend will handle the error page based on the error_type
|
||||
def redirect_to_error_page(error_type:, code:, error_message:)
|
||||
redirect_to app_new_tiktok_inbox_url(
|
||||
account_id: account_id,
|
||||
error_type: error_type,
|
||||
code: code,
|
||||
error_message: error_message
|
||||
)
|
||||
end
|
||||
|
||||
def find_or_create_inbox
|
||||
business_details = tiktok_client.business_account_details
|
||||
channel_tiktok = find_channel
|
||||
channel_exists = channel_tiktok.present?
|
||||
|
||||
if channel_tiktok
|
||||
update_channel(channel_tiktok, business_details)
|
||||
else
|
||||
channel_tiktok = create_channel_with_inbox(business_details)
|
||||
end
|
||||
|
||||
# reauthorized will also update cache keys for the associated inbox
|
||||
channel_tiktok.reauthorized!
|
||||
|
||||
set_avatar(channel_tiktok.inbox, business_details[:profile_image]) if business_details[:profile_image].present?
|
||||
|
||||
[channel_tiktok.inbox, channel_exists]
|
||||
end
|
||||
|
||||
def create_channel_with_inbox(business_details)
|
||||
ActiveRecord::Base.transaction do
|
||||
channel_tiktok = Channel::Tiktok.create!(
|
||||
account: account,
|
||||
business_id: short_term_access_token[:business_id],
|
||||
access_token: short_term_access_token[:access_token],
|
||||
refresh_token: short_term_access_token[:refresh_token],
|
||||
expires_at: short_term_access_token[:expires_at],
|
||||
refresh_token_expires_at: short_term_access_token[:refresh_token_expires_at]
|
||||
)
|
||||
|
||||
account.inboxes.create!(
|
||||
account: account,
|
||||
channel: channel_tiktok,
|
||||
name: business_details[:display_name].presence || business_details[:username]
|
||||
)
|
||||
|
||||
channel_tiktok
|
||||
end
|
||||
end
|
||||
|
||||
def find_channel
|
||||
Channel::Tiktok.find_by(business_id: short_term_access_token[:business_id], account: account)
|
||||
end
|
||||
|
||||
def update_channel(channel_tiktok, business_details)
|
||||
channel_tiktok.update!(
|
||||
access_token: short_term_access_token[:access_token],
|
||||
refresh_token: short_term_access_token[:refresh_token],
|
||||
expires_at: short_term_access_token[:expires_at],
|
||||
refresh_token_expires_at: short_term_access_token[:refresh_token_expires_at]
|
||||
)
|
||||
|
||||
channel_tiktok.inbox.update!(name: business_details[:display_name].presence || business_details[:username])
|
||||
end
|
||||
|
||||
def set_avatar(inbox, avatar_url)
|
||||
Avatar::AvatarFromUrlJob.perform_later(inbox, avatar_url)
|
||||
end
|
||||
|
||||
def account_id
|
||||
@account_id ||= verify_tiktok_token(params[:state])
|
||||
end
|
||||
|
||||
def account
|
||||
@account ||= Account.find(account_id)
|
||||
end
|
||||
|
||||
def short_term_access_token
|
||||
@short_term_access_token ||= Tiktok::AuthClient.obtain_short_term_access_token(params[:code])
|
||||
end
|
||||
|
||||
def tiktok_client
|
||||
@tiktok_client ||= Tiktok::Client.new(
|
||||
business_id: short_term_access_token[:business_id],
|
||||
access_token: short_term_access_token[:access_token]
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,53 @@
|
||||
class Webhooks::TiktokController < ActionController::API
|
||||
before_action :verify_signature!
|
||||
|
||||
def events
|
||||
event = JSON.parse(request_payload)
|
||||
if echo_event?
|
||||
# Add delay to prevent race condition where echo arrives before send message API completes
|
||||
# This avoids duplicate messages when echo comes early during API processing
|
||||
::Webhooks::TiktokEventsJob.set(wait: 2.seconds).perform_later(event)
|
||||
else
|
||||
::Webhooks::TiktokEventsJob.perform_later(event)
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request_payload
|
||||
@request_payload ||= request.body.read
|
||||
end
|
||||
|
||||
def verify_signature!
|
||||
signature_header = request.headers['Tiktok-Signature']
|
||||
client_secret = GlobalConfigService.load('TIKTOK_APP_SECRET', nil)
|
||||
received_timestamp, received_signature = extract_signature_parts(signature_header)
|
||||
|
||||
return head :unauthorized unless client_secret && received_timestamp && received_signature
|
||||
|
||||
signature_payload = "#{received_timestamp}.#{request_payload}"
|
||||
computed_signature = OpenSSL::HMAC.hexdigest('SHA256', client_secret, signature_payload)
|
||||
|
||||
return head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(computed_signature, received_signature)
|
||||
|
||||
# Check timestamp delay (acceptable delay: 5 seconds)
|
||||
current_timestamp = Time.current.to_i
|
||||
delay = current_timestamp - received_timestamp
|
||||
|
||||
return head :unauthorized if delay > 5
|
||||
end
|
||||
|
||||
def extract_signature_parts(signature_header)
|
||||
return [nil, nil] if signature_header.blank?
|
||||
|
||||
keys = signature_header.split(',')
|
||||
signature_parts = keys.map { |part| part.split('=') }.to_h
|
||||
[signature_parts['t']&.to_i, signature_parts['s']]
|
||||
end
|
||||
|
||||
def echo_event?
|
||||
params[:event] == 'im_send_msg'
|
||||
end
|
||||
end
|
||||
@@ -78,6 +78,12 @@ instagram:
|
||||
enabled: true
|
||||
icon: 'icon-instagram'
|
||||
config_key: 'instagram'
|
||||
tiktok:
|
||||
name: 'TikTok'
|
||||
description: 'Stay connected with your customers on TikTok'
|
||||
enabled: true
|
||||
icon: 'icon-tiktok'
|
||||
config_key: 'tiktok'
|
||||
whatsapp:
|
||||
name: 'WhatsApp'
|
||||
description: 'Manage your WhatsApp business interactions from Chatwoot.'
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
module Tiktok::IntegrationHelper
|
||||
# Generates a signed JWT token for Tiktok integration
|
||||
#
|
||||
# @param account_id [Integer] The account ID to encode in the token
|
||||
# @return [String, nil] The encoded JWT token or nil if client secret is missing
|
||||
def generate_tiktok_token(account_id)
|
||||
return if client_secret.blank?
|
||||
|
||||
JWT.encode(token_payload(account_id), client_secret, 'HS256')
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Failed to generate TikTok token: #{e.message}")
|
||||
nil
|
||||
end
|
||||
|
||||
# Verifies and decodes a Tiktok JWT token
|
||||
#
|
||||
# @param token [String] The JWT token to verify
|
||||
# @return [Integer, nil] The account ID from the token or nil if invalid
|
||||
def verify_tiktok_token(token)
|
||||
return if token.blank? || client_secret.blank?
|
||||
|
||||
decode_token(token, client_secret)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def client_secret
|
||||
@client_secret ||= GlobalConfigService.load('TIKTOK_APP_SECRET', nil)
|
||||
end
|
||||
|
||||
def token_payload(account_id)
|
||||
{
|
||||
sub: account_id,
|
||||
iat: Time.current.to_i
|
||||
}
|
||||
end
|
||||
|
||||
def decode_token(token, secret)
|
||||
JWT.decode(token, secret, true, {
|
||||
algorithm: 'HS256',
|
||||
verify_expiration: true
|
||||
}).first['sub']
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Unexpected error verifying Tiktok token: #{e.message}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../ApiClient';
|
||||
|
||||
class TiktokChannel extends ApiClient {
|
||||
constructor() {
|
||||
super('tiktok', { accountScoped: true });
|
||||
}
|
||||
|
||||
generateAuthorization(payload) {
|
||||
return axios.post(`${this.url}/authorization`, payload);
|
||||
}
|
||||
}
|
||||
|
||||
export default new TiktokChannel();
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Device } from '@twilio/voice-sdk';
|
||||
import VoiceAPI from './voiceAPIClient';
|
||||
|
||||
const createCallDisconnectedEvent = () => new CustomEvent('call:disconnected');
|
||||
|
||||
class TwilioVoiceClient extends EventTarget {
|
||||
constructor() {
|
||||
super();
|
||||
this.device = null;
|
||||
this.activeConnection = null;
|
||||
this.initialized = false;
|
||||
this.inboxId = null;
|
||||
}
|
||||
|
||||
async initializeDevice(inboxId) {
|
||||
this.destroyDevice();
|
||||
|
||||
const response = await VoiceAPI.getToken(inboxId);
|
||||
const { token, account_id } = response || {};
|
||||
if (!token) throw new Error('Invalid token');
|
||||
|
||||
this.device = new Device(token, {
|
||||
allowIncomingWhileBusy: true,
|
||||
disableAudioContextSounds: true,
|
||||
appParams: { account_id },
|
||||
});
|
||||
|
||||
this.device.removeAllListeners();
|
||||
this.device.on('connect', conn => {
|
||||
this.activeConnection = conn;
|
||||
conn.on('disconnect', this.onDisconnect);
|
||||
});
|
||||
|
||||
this.device.on('disconnect', this.onDisconnect);
|
||||
|
||||
this.device.on('tokenWillExpire', async () => {
|
||||
const r = await VoiceAPI.getToken(this.inboxId);
|
||||
if (r?.token) this.device.updateToken(r.token);
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
this.inboxId = inboxId;
|
||||
|
||||
return this.device;
|
||||
}
|
||||
|
||||
get hasActiveConnection() {
|
||||
return !!this.activeConnection;
|
||||
}
|
||||
|
||||
endClientCall() {
|
||||
if (this.activeConnection) {
|
||||
this.activeConnection.disconnect();
|
||||
}
|
||||
this.activeConnection = null;
|
||||
if (this.device) {
|
||||
this.device.disconnectAll();
|
||||
}
|
||||
}
|
||||
|
||||
destroyDevice() {
|
||||
if (this.device) {
|
||||
this.device.destroy();
|
||||
}
|
||||
this.activeConnection = null;
|
||||
this.device = null;
|
||||
this.initialized = false;
|
||||
this.inboxId = null;
|
||||
}
|
||||
|
||||
async joinClientCall({ to, conversationId }) {
|
||||
if (!this.device || !this.initialized || !to) return null;
|
||||
if (this.activeConnection) return this.activeConnection;
|
||||
|
||||
const params = {
|
||||
To: to,
|
||||
is_agent: 'true',
|
||||
conversation_id: conversationId,
|
||||
};
|
||||
|
||||
const connection = await this.device.connect({ params });
|
||||
this.activeConnection = connection;
|
||||
|
||||
connection.on('disconnect', this.onDisconnect);
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
onDisconnect = () => {
|
||||
this.activeConnection = null;
|
||||
this.dispatchEvent(createCallDisconnectedEvent());
|
||||
};
|
||||
}
|
||||
|
||||
export default new TwilioVoiceClient();
|
||||
@@ -0,0 +1,40 @@
|
||||
/* global axios */
|
||||
import ApiClient from '../../ApiClient';
|
||||
import ContactsAPI from '../../contacts';
|
||||
|
||||
class VoiceAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('voice', { accountScoped: true });
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
initiateCall(contactId, inboxId) {
|
||||
return ContactsAPI.initiateCall(contactId, inboxId).then(r => r.data);
|
||||
}
|
||||
|
||||
leaveConference(inboxId, conversationId) {
|
||||
return axios
|
||||
.delete(`${this.baseUrl()}/inboxes/${inboxId}/conference`, {
|
||||
params: { conversation_id: conversationId },
|
||||
})
|
||||
.then(r => r.data);
|
||||
}
|
||||
|
||||
joinConference({ conversationId, inboxId, callSid }) {
|
||||
return axios
|
||||
.post(`${this.baseUrl()}/inboxes/${inboxId}/conference`, {
|
||||
conversation_id: conversationId,
|
||||
call_sid: callSid,
|
||||
})
|
||||
.then(r => r.data);
|
||||
}
|
||||
|
||||
getToken(inboxId) {
|
||||
if (!inboxId) return Promise.reject(new Error('Inbox ID is required'));
|
||||
return axios
|
||||
.get(`${this.baseUrl()}/inboxes/${inboxId}/conference/token`)
|
||||
.then(r => r.data);
|
||||
}
|
||||
}
|
||||
|
||||
export default new VoiceAPI();
|
||||
@@ -0,0 +1,35 @@
|
||||
import ApiClient from '../ApiClient';
|
||||
import tiktokClient from '../channel/tiktokClient';
|
||||
|
||||
describe('#TiktokClient', () => {
|
||||
it('creates correct instance', () => {
|
||||
expect(tiktokClient).toBeInstanceOf(ApiClient);
|
||||
expect(tiktokClient).toHaveProperty('generateAuthorization');
|
||||
});
|
||||
|
||||
describe('#generateAuthorization', () => {
|
||||
const originalAxios = window.axios;
|
||||
const originalPathname = window.location.pathname;
|
||||
const axiosMock = {
|
||||
post: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.axios = axiosMock;
|
||||
window.history.pushState({}, '', '/app/accounts/1/settings');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.axios = originalAxios;
|
||||
window.history.pushState({}, '', originalPathname);
|
||||
});
|
||||
|
||||
it('posts to the authorization endpoint', () => {
|
||||
tiktokClient.generateAuthorization({ state: 'test-state' });
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/accounts/1/tiktok/authorization',
|
||||
{ state: 'test-state' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -44,6 +44,7 @@ const SOCIAL_CONFIG = {
|
||||
LINKEDIN: 'i-ri-linkedin-box-fill',
|
||||
FACEBOOK: 'i-ri-facebook-circle-fill',
|
||||
INSTAGRAM: 'i-ri-instagram-line',
|
||||
TIKTOK: 'i-ri-tiktok-fill',
|
||||
TWITTER: 'i-ri-twitter-x-fill',
|
||||
GITHUB: 'i-ri-github-fill',
|
||||
};
|
||||
@@ -65,6 +66,7 @@ const defaultState = {
|
||||
facebook: '',
|
||||
github: '',
|
||||
instagram: '',
|
||||
tiktok: '',
|
||||
linkedin: '',
|
||||
twitter: '',
|
||||
},
|
||||
|
||||
+6
-1
@@ -40,7 +40,12 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog ref="dialogRef" width="3xl" @confirm="handleDialogConfirm">
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
width="3xl"
|
||||
overflow-y-auto
|
||||
@confirm="handleDialogConfirm"
|
||||
>
|
||||
<ContactsForm
|
||||
ref="contactsFormRef"
|
||||
is-new-contact
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useMapGetter, useStore } from 'dashboard/composables/store';
|
||||
import { INBOX_TYPES } from 'dashboard/helper/inbox';
|
||||
import { useAlert } from 'dashboard/composables';
|
||||
import { frontendURL, conversationUrl } from 'dashboard/helper/URLHelper';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Dialog from 'dashboard/components-next/dialog/Dialog.vue';
|
||||
@@ -67,6 +68,17 @@ const startCall = async inboxId => {
|
||||
contactId: props.contactId,
|
||||
inboxId,
|
||||
});
|
||||
const { call_sid: callSid, conversation_id: conversationId } = response;
|
||||
|
||||
// Add call to store immediately so widget shows
|
||||
const callsStore = useCallsStore();
|
||||
callsStore.addCall({
|
||||
callSid,
|
||||
conversationId,
|
||||
inboxId,
|
||||
callDirection: 'outbound',
|
||||
});
|
||||
|
||||
useAlert(t('CONTACT_PANEL.CALL_INITIATED'));
|
||||
navigateToConversation(response?.conversation_id);
|
||||
} catch (error) {
|
||||
|
||||
+7
-4
@@ -171,10 +171,13 @@ const onPaste = e => {
|
||||
const files = e.clipboardData?.files;
|
||||
if (!files?.length) return;
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
const { name, type, size } = file;
|
||||
onFileUpload({ file, name, type, size });
|
||||
});
|
||||
// Filter valid files (non-zero size)
|
||||
Array.from(files)
|
||||
.filter(file => file.size > 0)
|
||||
.forEach(file => {
|
||||
const { name, type, size } = file;
|
||||
onFileUpload({ file, name, type, size });
|
||||
});
|
||||
};
|
||||
|
||||
useEventListener(document, 'paste', onPaste);
|
||||
|
||||
@@ -146,7 +146,7 @@ const STYLE_CONFIG = {
|
||||
solid:
|
||||
'bg-n-teal-9 text-white hover:enabled:bg-n-teal-10 focus-visible:bg-n-teal-10 outline-transparent',
|
||||
faded:
|
||||
'bg-n-teal-9/10 text-n-slate-12 hover:enabled:bg-n-teal-9/20 focus-visible:bg-n-teal-9/20 outline-transparent',
|
||||
'bg-n-teal-9/10 text-n-teal-11 hover:enabled:bg-n-teal-9/20 focus-visible:bg-n-teal-9/20 outline-transparent',
|
||||
outline:
|
||||
'text-n-teal-11 hover:enabled:bg-n-teal-9/10 focus-visible:bg-n-teal-9/10 outline-n-teal-9',
|
||||
link: 'text-n-teal-9 hover:enabled:underline focus-visible:underline outline-transparent',
|
||||
|
||||
@@ -13,6 +13,7 @@ export function useChannelIcon(inbox) {
|
||||
'Channel::WebWidget': 'i-woot-website',
|
||||
'Channel::Whatsapp': 'i-woot-whatsapp',
|
||||
'Channel::Instagram': 'i-woot-instagram',
|
||||
'Channel::Tiktok': 'i-woot-tiktok',
|
||||
'Channel::Voice': 'i-ri-phone-fill',
|
||||
};
|
||||
|
||||
|
||||
@@ -61,6 +61,12 @@ describe('useChannelIcon', () => {
|
||||
expect(icon).toBe('i-woot-instagram');
|
||||
});
|
||||
|
||||
it('returns correct icon for TikTok channel', () => {
|
||||
const inbox = { channel_type: 'Channel::Tiktok' };
|
||||
const { value: icon } = useChannelIcon(inbox);
|
||||
expect(icon).toBe('i-woot-tiktok');
|
||||
});
|
||||
|
||||
describe('TwilioSms channel', () => {
|
||||
it('returns chat icon for regular Twilio SMS channel', () => {
|
||||
const inbox = { channel_type: 'Channel::TwilioSms' };
|
||||
|
||||
@@ -28,6 +28,7 @@ import ImageBubble from './bubbles/Image.vue';
|
||||
import FileBubble from './bubbles/File.vue';
|
||||
import AudioBubble from './bubbles/Audio.vue';
|
||||
import VideoBubble from './bubbles/Video.vue';
|
||||
import EmbedBubble from './bubbles/Embed.vue';
|
||||
import InstagramStoryBubble from './bubbles/InstagramStory.vue';
|
||||
import EmailBubble from './bubbles/Email/Index.vue';
|
||||
import UnsupportedBubble from './bubbles/Unsupported.vue';
|
||||
@@ -317,6 +318,7 @@ const componentToRender = computed(() => {
|
||||
if (fileType === ATTACHMENT_TYPES.AUDIO) return AudioBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.VIDEO) return VideoBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.IG_REEL) return VideoBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.EMBED) return EmbedBubble;
|
||||
if (fileType === ATTACHMENT_TYPES.LOCATION) return LocationBubble;
|
||||
}
|
||||
// Attachment content is the name of the contact
|
||||
|
||||
@@ -20,6 +20,7 @@ const {
|
||||
isAWhatsAppChannel,
|
||||
isAnEmailChannel,
|
||||
isAnInstagramChannel,
|
||||
isATiktokChannel,
|
||||
} = useInbox();
|
||||
|
||||
const {
|
||||
@@ -60,7 +61,8 @@ const isSent = computed(() => {
|
||||
isAFacebookInbox.value ||
|
||||
isASmsInbox.value ||
|
||||
isATelegramChannel.value ||
|
||||
isAnInstagramChannel.value
|
||||
isAnInstagramChannel.value ||
|
||||
isATiktokChannel.value
|
||||
) {
|
||||
return sourceId.value && status.value === MESSAGE_STATUS.SENT;
|
||||
}
|
||||
@@ -78,7 +80,8 @@ const isDelivered = computed(() => {
|
||||
isAWhatsAppChannel.value ||
|
||||
isATwilioChannel.value ||
|
||||
isASmsInbox.value ||
|
||||
isAFacebookInbox.value
|
||||
isAFacebookInbox.value ||
|
||||
isATiktokChannel.value
|
||||
) {
|
||||
return sourceId.value && status.value === MESSAGE_STATUS.DELIVERED;
|
||||
}
|
||||
@@ -100,7 +103,8 @@ const isRead = computed(() => {
|
||||
isAWhatsAppChannel.value ||
|
||||
isATwilioChannel.value ||
|
||||
isAFacebookInbox.value ||
|
||||
isAnInstagramChannel.value
|
||||
isAnInstagramChannel.value ||
|
||||
isATiktokChannel.value
|
||||
) {
|
||||
return sourceId.value && status.value === MESSAGE_STATUS.READ;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import BaseBubble from './Base.vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { attachments } = useMessageContext();
|
||||
const { t } = useI18n();
|
||||
|
||||
const attachment = computed(() => {
|
||||
return attachments.value[0];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BaseBubble class="overflow-hidden p-3" data-bubble-name="embed">
|
||||
<div
|
||||
class="w-full max-w-[360px] sm:max-w-[420px] min-h-[520px] h-[70vh] max-h-[680px]"
|
||||
>
|
||||
<iframe
|
||||
class="w-full h-full border-0 rounded-lg"
|
||||
:title="t('CHAT_LIST.ATTACHMENTS.embed.CONTENT')"
|
||||
:src="attachment.dataUrl"
|
||||
loading="lazy"
|
||||
allow="autoplay; encrypted-media; picture-in-picture"
|
||||
allowfullscreen
|
||||
/>
|
||||
</div>
|
||||
</BaseBubble>
|
||||
</template>
|
||||
@@ -49,6 +49,7 @@ export const ATTACHMENT_TYPES = {
|
||||
STORY_MENTION: 'story_mention',
|
||||
CONTACT: 'contact',
|
||||
IG_REEL: 'ig_reel',
|
||||
EMBED: 'embed',
|
||||
IG_POST: 'ig_post',
|
||||
IG_STORY: 'ig_story',
|
||||
};
|
||||
|
||||
@@ -23,6 +23,10 @@ const hasInstagramConfigured = computed(() => {
|
||||
return window.chatwootConfig?.instagramAppId;
|
||||
});
|
||||
|
||||
const hasTiktokConfigured = computed(() => {
|
||||
return window.chatwootConfig?.tiktokAppId;
|
||||
});
|
||||
|
||||
const isActive = computed(() => {
|
||||
const { key } = props.channel;
|
||||
if (Object.keys(props.enabledFeatures).length === 0) {
|
||||
@@ -44,6 +48,10 @@ const isActive = computed(() => {
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'tiktok') {
|
||||
return props.enabledFeatures.channel_tiktok && hasTiktokConfigured.value;
|
||||
}
|
||||
|
||||
if (key === 'voice') {
|
||||
return props.enabledFeatures.channel_voice;
|
||||
}
|
||||
@@ -57,6 +65,7 @@ const isActive = computed(() => {
|
||||
'telegram',
|
||||
'line',
|
||||
'instagram',
|
||||
'tiktok',
|
||||
'voice',
|
||||
].includes(key);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
<script setup>
|
||||
import { watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useStore } from 'vuex';
|
||||
import { useCallSession } from 'dashboard/composables/useCallSession';
|
||||
import WindowVisibilityHelper from 'dashboard/helper/AudioAlerts/WindowVisibilityHelper';
|
||||
import Avatar from 'dashboard/components-next/avatar/Avatar.vue';
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const {
|
||||
activeCall,
|
||||
incomingCalls,
|
||||
hasActiveCall,
|
||||
isJoining,
|
||||
joinCall,
|
||||
endCall: endCallSession,
|
||||
rejectIncomingCall,
|
||||
dismissCall,
|
||||
formattedCallDuration,
|
||||
} = useCallSession();
|
||||
|
||||
const getCallInfo = call => {
|
||||
const conversation = store.getters.getConversationById(call?.conversationId);
|
||||
const inbox = store.getters['inboxes/getInbox'](conversation?.inbox_id);
|
||||
const sender = conversation?.meta?.sender;
|
||||
return {
|
||||
conversation,
|
||||
inbox,
|
||||
contactName: sender?.name || sender?.phone_number || 'Unknown caller',
|
||||
inboxName: inbox?.name || 'Customer support',
|
||||
avatar: sender?.avatar || sender?.thumbnail,
|
||||
};
|
||||
};
|
||||
|
||||
const handleEndCall = async () => {
|
||||
const call = activeCall.value;
|
||||
if (!call) return;
|
||||
|
||||
const inboxId = call.inboxId || getCallInfo(call).conversation?.inbox_id;
|
||||
if (!inboxId) return;
|
||||
|
||||
await endCallSession({
|
||||
conversationId: call.conversationId,
|
||||
inboxId,
|
||||
});
|
||||
};
|
||||
|
||||
const handleJoinCall = async call => {
|
||||
const { conversation } = getCallInfo(call);
|
||||
if (!call || !conversation || isJoining.value) return;
|
||||
|
||||
// End current active call before joining new one
|
||||
if (hasActiveCall.value) {
|
||||
await handleEndCall();
|
||||
}
|
||||
|
||||
const result = await joinCall({
|
||||
conversationId: call.conversationId,
|
||||
inboxId: conversation.inbox_id,
|
||||
callSid: call.callSid,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
router.push({
|
||||
name: 'inbox_conversation',
|
||||
params: { conversation_id: call.conversationId },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Auto-join outbound calls when window is visible
|
||||
watch(
|
||||
() => incomingCalls.value[0],
|
||||
call => {
|
||||
if (
|
||||
call?.callDirection === 'outbound' &&
|
||||
!hasActiveCall.value &&
|
||||
WindowVisibilityHelper.isWindowVisible()
|
||||
) {
|
||||
handleJoinCall(call);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="incomingCalls.length || hasActiveCall"
|
||||
class="fixed ltr:right-4 rtl:left-4 bottom-4 z-50 flex flex-col gap-2 w-72"
|
||||
>
|
||||
<!-- Incoming Calls (shown above active call) -->
|
||||
<div
|
||||
v-for="call in hasActiveCall ? incomingCalls : []"
|
||||
:key="call.callSid"
|
||||
class="flex items-center gap-3 p-4 bg-n-solid-2 rounded-xl shadow-xl outline outline-1 outline-n-strong"
|
||||
>
|
||||
<div class="animate-pulse ring-2 ring-n-teal-9 rounded-full inline-flex">
|
||||
<Avatar
|
||||
:src="getCallInfo(call).avatar"
|
||||
:name="getCallInfo(call).contactName"
|
||||
:size="40"
|
||||
rounded-full
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-n-slate-12 truncate mb-0">
|
||||
{{ getCallInfo(call).contactName }}
|
||||
</p>
|
||||
<p class="text-xs text-n-slate-11 truncate">
|
||||
{{ getCallInfo(call).inboxName }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<button
|
||||
class="flex justify-center items-center w-10 h-10 bg-n-ruby-9 hover:bg-n-ruby-10 rounded-full transition-colors"
|
||||
@click="dismissCall(call.callSid)"
|
||||
>
|
||||
<i class="text-lg text-white i-ph-phone-x-bold" />
|
||||
</button>
|
||||
<button
|
||||
class="flex justify-center items-center w-10 h-10 bg-n-teal-9 hover:bg-n-teal-10 rounded-full transition-colors"
|
||||
@click="handleJoinCall(call)"
|
||||
>
|
||||
<i class="text-lg text-white i-ph-phone-bold" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Call Widget -->
|
||||
<div
|
||||
v-if="hasActiveCall || incomingCalls.length"
|
||||
class="flex items-center gap-3 p-4 bg-n-solid-2 rounded-xl shadow-xl outline outline-1 outline-n-strong"
|
||||
>
|
||||
<div
|
||||
class="ring-2 ring-n-teal-9 rounded-full inline-flex"
|
||||
:class="{ 'animate-pulse': !hasActiveCall }"
|
||||
>
|
||||
<Avatar
|
||||
:src="getCallInfo(activeCall || incomingCalls[0]).avatar"
|
||||
:name="getCallInfo(activeCall || incomingCalls[0]).contactName"
|
||||
:size="40"
|
||||
rounded-full
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-n-slate-12 truncate mb-0">
|
||||
{{ getCallInfo(activeCall || incomingCalls[0]).contactName }}
|
||||
</p>
|
||||
<p v-if="hasActiveCall" class="font-mono text-sm text-n-teal-9">
|
||||
{{ formattedCallDuration }}
|
||||
</p>
|
||||
<p v-else class="text-xs text-n-slate-11">
|
||||
{{
|
||||
incomingCalls[0]?.callDirection === 'outbound'
|
||||
? $t('CONVERSATION.VOICE_WIDGET.OUTGOING_CALL')
|
||||
: $t('CONVERSATION.VOICE_WIDGET.INCOMING_CALL')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 gap-2">
|
||||
<button
|
||||
class="flex justify-center items-center w-10 h-10 bg-n-ruby-9 hover:bg-n-ruby-10 rounded-full transition-colors"
|
||||
@click="
|
||||
hasActiveCall
|
||||
? handleEndCall()
|
||||
: rejectIncomingCall(incomingCalls[0]?.callSid)
|
||||
"
|
||||
>
|
||||
<i class="text-lg text-white i-ph-phone-x-bold" />
|
||||
</button>
|
||||
<button
|
||||
v-if="!hasActiveCall"
|
||||
class="flex justify-center items-center w-10 h-10 bg-n-teal-9 hover:bg-n-teal-10 rounded-full transition-colors"
|
||||
@click="handleJoinCall(incomingCalls[0])"
|
||||
>
|
||||
<i class="text-lg text-white i-ph-phone-bold" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -676,11 +676,18 @@ function createEditorView() {
|
||||
typingIndicator.stop();
|
||||
emit('blur');
|
||||
},
|
||||
paste: (_view, event) => {
|
||||
paste: (view, event) => {
|
||||
if (props.disabled) return;
|
||||
const data = event.clipboardData.files;
|
||||
if (data.length > 0) {
|
||||
event.preventDefault();
|
||||
const { files } = event.clipboardData;
|
||||
if (!files.length) return;
|
||||
event.preventDefault();
|
||||
// Paste text content alongside files (e.g., spreadsheet data from Numbers app)
|
||||
// Numbers app includes invalid 0-byte attachments with text, so we paste the text here
|
||||
// while ReplyBox filters and handles valid file attachments
|
||||
const text = event.clipboardData.getData('text/plain');
|
||||
if (text) {
|
||||
view.dispatch(view.state.tr.insertText(text));
|
||||
emitOnChange();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -205,6 +205,9 @@ export default {
|
||||
if (this.isAWhatsAppCloudChannel) {
|
||||
return REPLY_POLICY.WHATSAPP_CLOUD;
|
||||
}
|
||||
if (this.isATiktokChannel) {
|
||||
return REPLY_POLICY.TIKTOK;
|
||||
}
|
||||
if (!this.isAPIInbox) {
|
||||
return REPLY_POLICY.TWILIO_WHATSAPP;
|
||||
}
|
||||
@@ -218,6 +221,9 @@ export default {
|
||||
) {
|
||||
return this.$t('CONVERSATION.24_HOURS_WINDOW');
|
||||
}
|
||||
if (this.isATiktokChannel) {
|
||||
return this.$t('CONVERSATION.48_HOURS_WINDOW');
|
||||
}
|
||||
if (!this.isAPIInbox) {
|
||||
return this.$t('CONVERSATION.TWILIO_WHATSAPP_24_HOURS_WINDOW');
|
||||
}
|
||||
|
||||
@@ -234,6 +234,9 @@ export default {
|
||||
if (this.isAnInstagramChannel) {
|
||||
return MESSAGE_MAX_LENGTH.INSTAGRAM;
|
||||
}
|
||||
if (this.isATiktokChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TIKTOK;
|
||||
}
|
||||
if (this.isATwilioWhatsAppChannel) {
|
||||
return MESSAGE_MAX_LENGTH.TWILIO_WHATSAPP;
|
||||
}
|
||||
@@ -689,20 +692,20 @@ export default {
|
||||
},
|
||||
onPaste(e) {
|
||||
// Don't handle paste if compose new conversation modal is open
|
||||
if (this.newConversationModalActive) {
|
||||
return;
|
||||
}
|
||||
if (this.newConversationModalActive) return;
|
||||
|
||||
const data = e.clipboardData.files;
|
||||
if (!this.showRichContentEditor && data.length !== 0) {
|
||||
this.$refs.messageInput?.$el?.blur();
|
||||
}
|
||||
if (!data.length || !data[0]) {
|
||||
return;
|
||||
}
|
||||
data.forEach(file => {
|
||||
const { name, type, size } = file;
|
||||
this.onFileUpload({ name, type, size, file: file });
|
||||
});
|
||||
|
||||
// Filter valid files (non-zero size)
|
||||
Array.from(e.clipboardData.files)
|
||||
.filter(file => file.size > 0)
|
||||
.forEach(file => {
|
||||
const { name, type, size } = file;
|
||||
this.onFileUpload({ name, type, size, file });
|
||||
});
|
||||
},
|
||||
toggleUserMention(currentMentionState) {
|
||||
this.showUserMentions = currentMentionState;
|
||||
|
||||
@@ -48,6 +48,7 @@ const mockStore = createStore({
|
||||
12: { id: 12, channel_type: INBOX_TYPES.SMS },
|
||||
13: { id: 13, channel_type: INBOX_TYPES.INSTAGRAM },
|
||||
14: { id: 14, channel_type: INBOX_TYPES.VOICE },
|
||||
15: { id: 15, channel_type: INBOX_TYPES.TIKTOK },
|
||||
};
|
||||
return inboxes[id] || null;
|
||||
},
|
||||
@@ -215,6 +216,12 @@ describe('useInbox', () => {
|
||||
global: { plugins: [mockStore] },
|
||||
});
|
||||
expect(wrapper.vm.isAVoiceChannel).toBe(true);
|
||||
|
||||
// Test Tiktok
|
||||
wrapper = mount(createTestComponent(15), {
|
||||
global: { plugins: [mockStore] },
|
||||
});
|
||||
expect(wrapper.vm.isATiktokChannel).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -266,6 +273,7 @@ describe('useInbox', () => {
|
||||
'is360DialogWhatsAppChannel',
|
||||
'isAnEmailChannel',
|
||||
'isAnInstagramChannel',
|
||||
'isATiktokChannel',
|
||||
'isAVoiceChannel',
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import { computed, ref, watch, onUnmounted, onMounted } from 'vue';
|
||||
import VoiceAPI from 'dashboard/api/channel/voice/voiceAPIClient';
|
||||
import TwilioVoiceClient from 'dashboard/api/channel/voice/twilioVoiceClient';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
import Timer from 'dashboard/helper/Timer';
|
||||
|
||||
export function useCallSession() {
|
||||
const callsStore = useCallsStore();
|
||||
const isJoining = ref(false);
|
||||
const callDuration = ref(0);
|
||||
const durationTimer = new Timer(elapsed => {
|
||||
callDuration.value = elapsed;
|
||||
});
|
||||
|
||||
const activeCall = computed(() => callsStore.activeCall);
|
||||
const incomingCalls = computed(() => callsStore.incomingCalls);
|
||||
const hasActiveCall = computed(() => callsStore.hasActiveCall);
|
||||
|
||||
watch(
|
||||
hasActiveCall,
|
||||
active => {
|
||||
if (active) {
|
||||
durationTimer.start();
|
||||
} else {
|
||||
durationTimer.stop();
|
||||
callDuration.value = 0;
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
TwilioVoiceClient.addEventListener('call:disconnected', () =>
|
||||
callsStore.clearActiveCall()
|
||||
);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
durationTimer.stop();
|
||||
TwilioVoiceClient.removeEventListener('call:disconnected', () =>
|
||||
callsStore.clearActiveCall()
|
||||
);
|
||||
});
|
||||
|
||||
const endCall = async ({ conversationId, inboxId }) => {
|
||||
await VoiceAPI.leaveConference(inboxId, conversationId);
|
||||
TwilioVoiceClient.endClientCall();
|
||||
durationTimer.stop();
|
||||
callsStore.clearActiveCall();
|
||||
};
|
||||
|
||||
const joinCall = async ({ conversationId, inboxId, callSid }) => {
|
||||
if (isJoining.value) return null;
|
||||
|
||||
isJoining.value = true;
|
||||
try {
|
||||
const device = await TwilioVoiceClient.initializeDevice(inboxId);
|
||||
if (!device) return null;
|
||||
|
||||
const joinResponse = await VoiceAPI.joinConference({
|
||||
conversationId,
|
||||
inboxId,
|
||||
callSid,
|
||||
});
|
||||
|
||||
await TwilioVoiceClient.joinClientCall({
|
||||
to: joinResponse?.conference_sid,
|
||||
conversationId,
|
||||
});
|
||||
|
||||
callsStore.setCallActive(callSid);
|
||||
durationTimer.start();
|
||||
|
||||
return { conferenceSid: joinResponse?.conference_sid };
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to join call:', error);
|
||||
return null;
|
||||
} finally {
|
||||
isJoining.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const rejectIncomingCall = callSid => {
|
||||
TwilioVoiceClient.endClientCall();
|
||||
callsStore.dismissCall(callSid);
|
||||
};
|
||||
|
||||
const dismissCall = callSid => {
|
||||
callsStore.dismissCall(callSid);
|
||||
};
|
||||
|
||||
const formattedCallDuration = computed(() => {
|
||||
const minutes = Math.floor(callDuration.value / 60);
|
||||
const seconds = callDuration.value % 60;
|
||||
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
});
|
||||
|
||||
return {
|
||||
activeCall,
|
||||
incomingCalls,
|
||||
hasActiveCall,
|
||||
isJoining,
|
||||
formattedCallDuration,
|
||||
joinCall,
|
||||
endCall,
|
||||
rejectIncomingCall,
|
||||
dismissCall,
|
||||
};
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export const INBOX_FEATURE_MAP = {
|
||||
INBOX_TYPES.TWITTER,
|
||||
INBOX_TYPES.WHATSAPP,
|
||||
INBOX_TYPES.TELEGRAM,
|
||||
INBOX_TYPES.TIKTOK,
|
||||
INBOX_TYPES.API,
|
||||
],
|
||||
[INBOX_FEATURES.REPLY_TO_OUTGOING]: [
|
||||
@@ -24,6 +25,7 @@ export const INBOX_FEATURE_MAP = {
|
||||
INBOX_TYPES.TWITTER,
|
||||
INBOX_TYPES.WHATSAPP,
|
||||
INBOX_TYPES.TELEGRAM,
|
||||
INBOX_TYPES.TIKTOK,
|
||||
INBOX_TYPES.API,
|
||||
],
|
||||
};
|
||||
@@ -128,6 +130,10 @@ export const useInbox = (inboxId = null) => {
|
||||
return channelType.value === INBOX_TYPES.INSTAGRAM;
|
||||
});
|
||||
|
||||
const isATiktokChannel = computed(() => {
|
||||
return channelType.value === INBOX_TYPES.TIKTOK;
|
||||
});
|
||||
|
||||
const isAVoiceChannel = computed(() => {
|
||||
return channelType.value === INBOX_TYPES.VOICE;
|
||||
});
|
||||
@@ -149,6 +155,7 @@ export const useInbox = (inboxId = null) => {
|
||||
is360DialogWhatsAppChannel,
|
||||
isAnEmailChannel,
|
||||
isAnInstagramChannel,
|
||||
isATiktokChannel,
|
||||
isAVoiceChannel,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -109,6 +109,11 @@ export const FORMATTING = {
|
||||
nodes: [],
|
||||
menu: [],
|
||||
},
|
||||
'Channel::Tiktok': {
|
||||
marks: [],
|
||||
nodes: [],
|
||||
menu: [],
|
||||
},
|
||||
// Special contexts (not actual channels)
|
||||
'Context::Default': {
|
||||
marks: ['strong', 'em', 'code', 'link', 'strike'],
|
||||
|
||||
@@ -37,6 +37,7 @@ export const FEATURE_FLAGS = {
|
||||
CHATWOOT_V4: 'chatwoot_v4',
|
||||
REPORT_V4: 'report_v4',
|
||||
CHANNEL_INSTAGRAM: 'channel_instagram',
|
||||
CHANNEL_TIKTOK: 'channel_tiktok',
|
||||
CONTACT_CHATWOOT_SUPPORT_TEAM: 'contact_chatwoot_support_team',
|
||||
CAPTAIN_V2: 'captain_integration_v2',
|
||||
SAML: 'saml',
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
export default class Timer {
|
||||
constructor(onTick = null) {
|
||||
this.elapsed = 0;
|
||||
this.intervalId = null;
|
||||
this.onTick = onTick;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
}
|
||||
this.elapsed = 0;
|
||||
this.intervalId = setInterval(() => {
|
||||
this.elapsed += 1;
|
||||
if (this.onTick) {
|
||||
this.onTick(this.elapsed);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.intervalId) {
|
||||
clearInterval(this.intervalId);
|
||||
this.intervalId = null;
|
||||
}
|
||||
this.elapsed = 0;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export const INBOX_TYPES = {
|
||||
LINE: 'Channel::Line',
|
||||
SMS: 'Channel::Sms',
|
||||
INSTAGRAM: 'Channel::Instagram',
|
||||
TIKTOK: 'Channel::Tiktok',
|
||||
VOICE: 'Channel::Voice',
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ const INBOX_ICON_MAP_FILL = {
|
||||
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-fill',
|
||||
[INBOX_TYPES.LINE]: 'i-ri-line-fill',
|
||||
[INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-fill',
|
||||
[INBOX_TYPES.TIKTOK]: 'i-ri-tiktok-fill',
|
||||
[INBOX_TYPES.VOICE]: 'i-ri-phone-fill',
|
||||
};
|
||||
|
||||
@@ -43,6 +45,7 @@ const INBOX_ICON_MAP_LINE = {
|
||||
[INBOX_TYPES.TELEGRAM]: 'i-ri-telegram-line',
|
||||
[INBOX_TYPES.LINE]: 'i-ri-line-line',
|
||||
[INBOX_TYPES.INSTAGRAM]: 'i-ri-instagram-line',
|
||||
[INBOX_TYPES.TIKTOK]: 'i-ri-tiktok-line',
|
||||
[INBOX_TYPES.VOICE]: 'i-ri-phone-line',
|
||||
};
|
||||
|
||||
@@ -136,6 +139,9 @@ export const getInboxClassByType = (type, phoneNumber) => {
|
||||
case INBOX_TYPES.INSTAGRAM:
|
||||
return 'brand-instagram';
|
||||
|
||||
case INBOX_TYPES.TIKTOK:
|
||||
return 'brand-tiktok';
|
||||
|
||||
case INBOX_TYPES.VOICE:
|
||||
return 'phone';
|
||||
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import Timer from '../Timer';
|
||||
|
||||
describe('Timer', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('initializes with elapsed 0 and no interval', () => {
|
||||
const timer = new Timer();
|
||||
expect(timer.elapsed).toBe(0);
|
||||
expect(timer.intervalId).toBeNull();
|
||||
});
|
||||
|
||||
it('accepts an onTick callback', () => {
|
||||
const onTick = vi.fn();
|
||||
const timer = new Timer(onTick);
|
||||
expect(timer.onTick).toBe(onTick);
|
||||
});
|
||||
});
|
||||
|
||||
describe('start', () => {
|
||||
it('starts the timer and increments elapsed every second', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
|
||||
expect(timer.elapsed).toBe(0);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(timer.elapsed).toBe(1);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(timer.elapsed).toBe(2);
|
||||
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(timer.elapsed).toBe(5);
|
||||
});
|
||||
|
||||
it('calls onTick callback with elapsed value', () => {
|
||||
const onTick = vi.fn();
|
||||
const timer = new Timer(onTick);
|
||||
timer.start();
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(onTick).toHaveBeenCalledWith(1);
|
||||
|
||||
vi.advanceTimersByTime(1000);
|
||||
expect(onTick).toHaveBeenCalledWith(2);
|
||||
|
||||
expect(onTick).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('resets elapsed to 0 when restarted', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(timer.elapsed).toBe(5);
|
||||
|
||||
timer.start();
|
||||
expect(timer.elapsed).toBe(0);
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
expect(timer.elapsed).toBe(2);
|
||||
});
|
||||
|
||||
it('clears previous interval when restarted', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
const firstIntervalId = timer.intervalId;
|
||||
|
||||
timer.start();
|
||||
expect(timer.intervalId).not.toBe(firstIntervalId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stop', () => {
|
||||
it('stops the timer and resets elapsed to 0', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(timer.elapsed).toBe(3);
|
||||
|
||||
timer.stop();
|
||||
expect(timer.elapsed).toBe(0);
|
||||
expect(timer.intervalId).toBeNull();
|
||||
});
|
||||
|
||||
it('prevents further increments after stopping', () => {
|
||||
const timer = new Timer();
|
||||
timer.start();
|
||||
|
||||
vi.advanceTimersByTime(2000);
|
||||
timer.stop();
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
expect(timer.elapsed).toBe(0);
|
||||
});
|
||||
|
||||
it('handles stop when timer is not running', () => {
|
||||
const timer = new Timer();
|
||||
expect(() => timer.stop()).not.toThrow();
|
||||
expect(timer.elapsed).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -38,6 +38,9 @@ describe('#Inbox Helpers', () => {
|
||||
it('should return correct class for Email', () => {
|
||||
expect(getInboxClassByType('Channel::Email')).toEqual('mail');
|
||||
});
|
||||
it('should return correct class for TikTok', () => {
|
||||
expect(getInboxClassByType(INBOX_TYPES.TIKTOK)).toEqual('brand-tiktok');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getInboxIconByType', () => {
|
||||
@@ -80,6 +83,10 @@ describe('#Inbox Helpers', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.LINE)).toBe('i-ri-line-fill');
|
||||
});
|
||||
|
||||
it('returns correct icon for TikTok', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TIKTOK)).toBe('i-ri-tiktok-fill');
|
||||
});
|
||||
|
||||
it('returns default icon for unknown type', () => {
|
||||
expect(getInboxIconByType('UNKNOWN_TYPE')).toBe('i-ri-chat-1-fill');
|
||||
});
|
||||
@@ -102,6 +109,12 @@ describe('#Inbox Helpers', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct line icon for TikTok', () => {
|
||||
expect(getInboxIconByType(INBOX_TYPES.TIKTOK, null, 'line')).toBe(
|
||||
'i-ri-tiktok-line'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns correct line icon for unknown type', () => {
|
||||
expect(getInboxIconByType('UNKNOWN_TYPE', null, 'line')).toBe(
|
||||
'i-ri-chat-1-line'
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { CONTENT_TYPES } from 'dashboard/components-next/message/constants';
|
||||
import { useCallsStore } from 'dashboard/stores/calls';
|
||||
import types from 'dashboard/store/mutation-types';
|
||||
|
||||
export const TERMINAL_STATUSES = [
|
||||
'completed',
|
||||
'busy',
|
||||
'failed',
|
||||
'no-answer',
|
||||
'canceled',
|
||||
'missed',
|
||||
'ended',
|
||||
];
|
||||
|
||||
export const isInbound = direction => direction === 'inbound';
|
||||
|
||||
const isVoiceCallMessage = message => {
|
||||
return CONTENT_TYPES.VOICE_CALL === message?.content_type;
|
||||
};
|
||||
|
||||
const shouldSkipCall = (callDirection, senderId, currentUserId) => {
|
||||
return callDirection === 'outbound' && senderId !== currentUserId;
|
||||
};
|
||||
|
||||
function extractCallData(message) {
|
||||
const contentData = message?.content_attributes?.data || {};
|
||||
return {
|
||||
callSid: contentData.call_sid,
|
||||
status: contentData.status,
|
||||
callDirection: contentData.call_direction,
|
||||
conversationId: message?.conversation_id,
|
||||
senderId: message?.sender?.id,
|
||||
};
|
||||
}
|
||||
|
||||
export function handleVoiceCallCreated(message, currentUserId) {
|
||||
if (!isVoiceCallMessage(message)) return;
|
||||
|
||||
const { callSid, callDirection, conversationId, senderId } =
|
||||
extractCallData(message);
|
||||
|
||||
if (shouldSkipCall(callDirection, senderId, currentUserId)) return;
|
||||
|
||||
const callsStore = useCallsStore();
|
||||
callsStore.addCall({
|
||||
callSid,
|
||||
conversationId,
|
||||
callDirection,
|
||||
senderId,
|
||||
});
|
||||
}
|
||||
|
||||
export function handleVoiceCallUpdated(commit, message, currentUserId) {
|
||||
if (!isVoiceCallMessage(message)) return;
|
||||
|
||||
const { callSid, status, callDirection, conversationId, senderId } =
|
||||
extractCallData(message);
|
||||
|
||||
const callsStore = useCallsStore();
|
||||
|
||||
callsStore.handleCallStatusChanged({ callSid, status, conversationId });
|
||||
|
||||
const callInfo = { conversationId, callStatus: status };
|
||||
commit(types.UPDATE_CONVERSATION_CALL_STATUS, callInfo);
|
||||
commit(types.UPDATE_MESSAGE_CALL_STATUS, callInfo);
|
||||
|
||||
const isNewCall =
|
||||
status === 'ringing' &&
|
||||
!shouldSkipCall(callDirection, senderId, currentUserId);
|
||||
|
||||
if (isNewCall) {
|
||||
callsStore.addCall({
|
||||
callSid,
|
||||
conversationId,
|
||||
callDirection,
|
||||
senderId,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,10 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"COMPANIES": {
|
||||
"HEADER": "Companies",
|
||||
"SORT_BY": {
|
||||
"LABEL": "Sort by",
|
||||
"OPTIONS": {
|
||||
"NAME": "Name",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Created at"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
"LABEL": "Order",
|
||||
"OPTIONS": {
|
||||
"ASCENDING": "Ascending",
|
||||
"DESCENDING": "Descending"
|
||||
}
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Search companies...",
|
||||
"LOADING": "Loading companies...",
|
||||
"UNNAMED": "Unnamed Company",
|
||||
"CONTACTS_COUNT": "{n} contact | {n} contacts",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No companies found"
|
||||
}
|
||||
},
|
||||
"COMPANIES_LAYOUT": {
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "ደውል",
|
||||
"CALL_UNDER_DEVELOPMENT": "መደወል በልማት ላይ ነው",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "የድምፅ ኢንቦክስ ይምረጡ"
|
||||
},
|
||||
@@ -456,6 +457,9 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"CANNOT_REPLY": "You cannot reply due to",
|
||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"48_HOURS_WINDOW": "48 hour message window restriction",
|
||||
"API_HOURS_WINDOW": "ለዚህ ውይይት መመለስ በ{hours} ሰአታት ውስጥ ብቻ ይቻላል",
|
||||
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
||||
"ASSIGN_TO_ME": "Assign to me",
|
||||
@@ -196,7 +197,6 @@
|
||||
"INSERT_READ_MORE": "Read more",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"TIP_AUDIORECORDER_ICON": "Record audio",
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
|
||||
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You won’t be able to send/receive Instagram messages from this inbox anymore."
|
||||
},
|
||||
"TIKTOK": {
|
||||
"CONTINUE_WITH_TIKTOK": "Continue with TikTok",
|
||||
"CONNECT_YOUR_TIKTOK_PROFILE": "Connect your TikTok Profile",
|
||||
"HELP": "To add your TikTok profile as a channel, you need to authenticate your TikTok Profile by clicking on 'Continue with TikTok' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to TikTok, please try again",
|
||||
"ERROR_AUTH": "There was an error connecting to TikTok, please try again"
|
||||
},
|
||||
"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",
|
||||
@@ -381,7 +388,11 @@
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the email channel"
|
||||
},
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address.",
|
||||
"FINISH_MESSAGE_NO_FORWARDING": "Your email inbox has been created successfully! You need to configure SMTP and IMAP credentials to send and receive emails. Without these settings, no emails will be processed.",
|
||||
"FORWARDING_ADDRESS_LABEL": "Forward emails to this address:",
|
||||
"CONFIGURE_SMTP_IMAP_LINK": "Click here",
|
||||
"CONFIGURE_SMTP_IMAP_TEXT": " to configure IMAP and SMTP settings"
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "LINE Channel",
|
||||
@@ -467,6 +478,10 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -707,6 +722,7 @@
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.",
|
||||
"FORWARD_EMAIL_NOT_CONFIGURED": "Forwarding emails to your inbox is currently disabled on this installation. To use this feature, it must be enabled by your administrator. Please get in touch with them to proceed.",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved.",
|
||||
"WHATSAPP_SECTION_SUBHEADER": "This API Key is used for the integration with the WhatsApp APIs.",
|
||||
@@ -1004,6 +1020,7 @@
|
||||
"LINE": "Line",
|
||||
"API": "API Channel",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"CONTACT_SUPPORT": "Contact support",
|
||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||
"PROFILE_SETTINGS": "Profile settings",
|
||||
"YEAR_IN_REVIEW": "Year in Review",
|
||||
"KEYBOARD_SHORTCUTS": "Keyboard shortcuts",
|
||||
"APPEARANCE": "Change appearance",
|
||||
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
|
||||
@@ -306,6 +307,8 @@
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Contacts",
|
||||
"ACTIVE": "Active",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -398,19 +401,44 @@
|
||||
"BUTTON_TXT": "Buy more credits",
|
||||
"DOCUMENTS": "Documents",
|
||||
"RESPONSES": "Responses",
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more.",
|
||||
"REFRESH_CREDITS": "Refresh"
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "Need help?",
|
||||
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
|
||||
"BUTTON_TXT": "Chat with us"
|
||||
},
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again.",
|
||||
"TOPUP": {
|
||||
"BUY_CREDITS": "Buy more credits",
|
||||
"MODAL_TITLE": "Buy AI Credits",
|
||||
"MODAL_DESCRIPTION": "Purchase additional credits for Captain AI.",
|
||||
"CREDITS": "CREDITS",
|
||||
"ONE_TIME": "one-time",
|
||||
"POPULAR": "Most Popular",
|
||||
"NOTE_TITLE": "Note:",
|
||||
"NOTE_DESCRIPTION": "Credits are added immediately and expire in 6 months. An active subscription is required to use credits. Purchased credits are consumed after your monthly plan credits.",
|
||||
"CANCEL": "Cancel",
|
||||
"PURCHASE": "Purchase Credits",
|
||||
"LOADING": "Loading options...",
|
||||
"FETCH_ERROR": "Failed to load credit options. Please try again.",
|
||||
"PURCHASE_ERROR": "Failed to process purchase. Please try again.",
|
||||
"PURCHASE_SUCCESS": "Successfully added {credits} credits to your account",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Purchase",
|
||||
"DESCRIPTION": "You are about to purchase {credits} credits for {amount}.",
|
||||
"INSTANT_DEDUCTION_NOTE": "Your saved card will be charged immediately upon confirmation.",
|
||||
"GO_BACK": "Go Back",
|
||||
"CONFIRM_PURCHASE": "Confirm Purchase"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
"TITLE": "Security",
|
||||
"DESCRIPTION": "Manage your account security settings.",
|
||||
"LINK_TEXT": "Learn more about SAML SSO",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"YEAR_IN_REVIEW": {
|
||||
"TITLE": "Year in Review",
|
||||
"LOADING": "Loading your year in review...",
|
||||
"ERROR": "Failed to load year in review",
|
||||
"CLOSE": "Close",
|
||||
"CONVERSATIONS": {
|
||||
"TITLE": "You have handled",
|
||||
"SUBTITLE": "conversations",
|
||||
"FALLBACK": "This year wasn't about the numbers. It was about showing up.",
|
||||
"COMPARISON": {
|
||||
"0_50": "You showed up, and that's how every good inbox begins.",
|
||||
"50_100": "You kept the replies flowing and the conversations alive.",
|
||||
"100_500": "You handled serious volume and kept everything on track.",
|
||||
"500_2000": "You kept things moving while the volume kept climbing.",
|
||||
"2000_10000": "You ran high traffic through your inbox without breaking a sweat.",
|
||||
"10000_PLUS": "That's a full city of customers knocking on your door. You made it look effortless."
|
||||
}
|
||||
},
|
||||
"BUSIEST_DAY": {
|
||||
"TITLE": "Your busiest day was",
|
||||
"MESSAGE": "{count} conversations that day.",
|
||||
"COMPARISON": {
|
||||
"0_5": "A warm-up lap that barely woke the inbox.",
|
||||
"5_10": "Enough action to justify a second cup of coffee.",
|
||||
"10_25": "Things got busy and the inbox stayed on its toes.",
|
||||
"25_50": "A proper rush that barely broke a sweat.",
|
||||
"50_100": "Controlled chaos, handled like a normal Tuesday.",
|
||||
"100_500": "Absolute dumpster fire, somehow still shipping replies.",
|
||||
"500_PLUS": "The inbox lost all chill and never slowed down."
|
||||
}
|
||||
},
|
||||
"PERSONALITY": {
|
||||
"TITLE": "Your support personality is",
|
||||
"MESSAGES": {
|
||||
"SWIFT_HELPER": "You replied in {time} on average. Faster than most notifications.",
|
||||
"QUICK_RESPONDER": "You replied in {time} on average. The inbox barely waited.",
|
||||
"STEADY_SUPPORT": "You replied in {time} on average. Calm pace, solid replies.",
|
||||
"THOUGHTFUL_ADVISOR": "You replied in {time} on average. Took the time to get it right."
|
||||
}
|
||||
},
|
||||
"THANK_YOU": {
|
||||
"TITLE": "Congratulations on surviving the inbox of {year}.",
|
||||
"MESSAGE": "Thank you for your incredible dedication to supporting customers throughout this year. Your hard work has made a real difference, and we're grateful to have you on this journey. Here's to making {nextYear} even better together!"
|
||||
},
|
||||
"SHARE_MODAL": {
|
||||
"TITLE": "Share Your Year in Review",
|
||||
"PREPARING": "Preparing your image...",
|
||||
"DOWNLOAD": "Download",
|
||||
"SHARE_TITLE": "My {year} Year in Review",
|
||||
"SHARE_TEXT": "Check out my {year} Year in Review with Chatwoot!",
|
||||
"BRANDING": "Made with Chatwoot"
|
||||
},
|
||||
"BANNER": {
|
||||
"TITLE": "Your {year} Year in Review is here",
|
||||
"BUTTON": "See your impact"
|
||||
},
|
||||
"NAVIGATION": {
|
||||
"PREVIOUS": "Previous",
|
||||
"NEXT": "Next",
|
||||
"SHARE": "Share"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,10 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "تمكين التحقق من صحة regex"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"COMPANIES": {
|
||||
"HEADER": "Companies",
|
||||
"SORT_BY": {
|
||||
"LABEL": "ترتيب حسب",
|
||||
"OPTIONS": {
|
||||
"NAME": "الاسم",
|
||||
"DOMAIN": "النطاق",
|
||||
"CREATED_AT": "تم إنشاؤها في"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
"LABEL": "Order",
|
||||
"OPTIONS": {
|
||||
"ASCENDING": "Ascending",
|
||||
"DESCENDING": "Descending"
|
||||
}
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Search companies...",
|
||||
"LOADING": "Loading companies...",
|
||||
"UNNAMED": "Unnamed Company",
|
||||
"CONTACTS_COUNT": "{n} contact | {n} contacts",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No companies found"
|
||||
}
|
||||
},
|
||||
"COMPANIES_LAYOUT": {
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@
|
||||
"CREATED_AT_LABEL": "تم إنشاؤها",
|
||||
"NEW_MESSAGE": "رسالة جديدة",
|
||||
"CALL": "Call",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -456,6 +457,9 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"LOADING_CONVERSATIONS": "جاري جلب المحادثات",
|
||||
"CANNOT_REPLY": "لا يمكنك الرد بسبب",
|
||||
"24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
|
||||
"48_HOURS_WINDOW": "قيد نافذة الـ 48 ساعة",
|
||||
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
||||
"NOT_ASSIGNED_TO_YOU": "لم يتم تعيين هذه المحادثة لك. هل ترغب في تعيين هذه المحادثة لنفسك؟",
|
||||
"ASSIGN_TO_ME": "إسناد لي",
|
||||
@@ -196,7 +197,6 @@
|
||||
"INSERT_READ_MORE": "اقرأ المزيد",
|
||||
"DISMISS_REPLY": "استبعاد الرد",
|
||||
"REPLYING_TO": "الرد على:",
|
||||
"TIP_FORMAT_ICON": "عرض محرر النصوص",
|
||||
"TIP_EMOJI_ICON": "إظهار قائمة الرموز التعبيرية",
|
||||
"TIP_ATTACH_ICON": "إرفاق الملفات",
|
||||
"TIP_AUDIORECORDER_ICON": "تسجيل الصوت",
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
|
||||
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You won’t be able to send/receive Instagram messages from this inbox anymore."
|
||||
},
|
||||
"TIKTOK": {
|
||||
"CONTINUE_WITH_TIKTOK": "Continue with TikTok",
|
||||
"CONNECT_YOUR_TIKTOK_PROFILE": "Connect your TikTok Profile",
|
||||
"HELP": "To add your TikTok profile as a channel, you need to authenticate your TikTok Profile by clicking on 'Continue with TikTok' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to TikTok, please try again",
|
||||
"ERROR_AUTH": "There was an error connecting to TikTok, please try again"
|
||||
},
|
||||
"TWITTER": {
|
||||
"HELP": "لإضافة حساب تويتر الخاص بك كقناة تواصل، تحتاج إلى مصادقة حسابك على تويتر بك بالنقر على زر \"تسجيل الدخول باستخدام تويتر\" ",
|
||||
"ERROR_MESSAGE": "حدث خطأ أثناء الاتصال بـ Twitter، الرجاء المحاولة مرة أخرى",
|
||||
@@ -381,7 +388,11 @@
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "لم نتمكن من حفظ قناة البريد الإلكتروني"
|
||||
},
|
||||
"FINISH_MESSAGE": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي."
|
||||
"FINISH_MESSAGE": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي.",
|
||||
"FINISH_MESSAGE_NO_FORWARDING": "Your email inbox has been created successfully! You need to configure SMTP and IMAP credentials to send and receive emails. Without these settings, no emails will be processed.",
|
||||
"FORWARDING_ADDRESS_LABEL": "Forward emails to this address:",
|
||||
"CONFIGURE_SMTP_IMAP_LINK": "اضغط هنا",
|
||||
"CONFIGURE_SMTP_IMAP_TEXT": " to configure IMAP and SMTP settings"
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "قناة LINE",
|
||||
@@ -467,6 +478,10 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -707,6 +722,7 @@
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "استخدم رمز 'inbox_identifier' المعروض هنا للمصادقة على عملاء API الخاص بك.",
|
||||
"FORWARD_EMAIL_TITLE": "إعادة التوجيه إلى البريد الإلكتروني",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي.",
|
||||
"FORWARD_EMAIL_NOT_CONFIGURED": "Forwarding emails to your inbox is currently disabled on this installation. To use this feature, it must be enabled by your administrator. Please get in touch with them to proceed.",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "السماح بالرسائل بعد حل المحادثة",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "السماح للمستخدمين النهائيين بإرسال رسائل حتى بعد تسوية المحادثة.",
|
||||
"WHATSAPP_SECTION_SUBHEADER": "يتم استخدام مفتاح API هذا للتكامل مع واتسب APIs.",
|
||||
@@ -1004,6 +1020,7 @@
|
||||
"LINE": "Line",
|
||||
"API": "قناة API",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"CONTACT_SUPPORT": "Contact support",
|
||||
"SELECTOR_SUBTITLE": "اختر حساباً من القائمة التالية",
|
||||
"PROFILE_SETTINGS": "Profile settings",
|
||||
"YEAR_IN_REVIEW": "Year in Review",
|
||||
"KEYBOARD_SHORTCUTS": "Keyboard shortcuts",
|
||||
"APPEARANCE": "Change appearance",
|
||||
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
|
||||
@@ -306,6 +307,8 @@
|
||||
"SETTINGS": "الإعدادات",
|
||||
"CONTACTS": "جهات الاتصال",
|
||||
"ACTIVE": "مفعل",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "قائد",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -398,19 +401,44 @@
|
||||
"BUTTON_TXT": "Buy more credits",
|
||||
"DOCUMENTS": "Documents",
|
||||
"RESPONSES": "Responses",
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more.",
|
||||
"REFRESH_CREDITS": "تحديث"
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "تحتاج مساعدة؟",
|
||||
"DESCRIPTION": "هل تواجه أي مشاكل في الفواتير؟ نحن هنا للمساعدة.",
|
||||
"BUTTON_TXT": "تحدث الينا"
|
||||
},
|
||||
"NO_BILLING_USER": "حساب الفوترة الخاص بك قيد الإعداد. الرجاء تحديث الصفحة وحاول مرة أخرى."
|
||||
"NO_BILLING_USER": "حساب الفوترة الخاص بك قيد الإعداد. الرجاء تحديث الصفحة وحاول مرة أخرى.",
|
||||
"TOPUP": {
|
||||
"BUY_CREDITS": "Buy more credits",
|
||||
"MODAL_TITLE": "Buy AI Credits",
|
||||
"MODAL_DESCRIPTION": "Purchase additional credits for Captain AI.",
|
||||
"CREDITS": "CREDITS",
|
||||
"ONE_TIME": "one-time",
|
||||
"POPULAR": "Most Popular",
|
||||
"NOTE_TITLE": "ملاحظة:",
|
||||
"NOTE_DESCRIPTION": "Credits are added immediately and expire in 6 months. An active subscription is required to use credits. Purchased credits are consumed after your monthly plan credits.",
|
||||
"CANCEL": "إلغاء",
|
||||
"PURCHASE": "Purchase Credits",
|
||||
"LOADING": "Loading options...",
|
||||
"FETCH_ERROR": "Failed to load credit options. Please try again.",
|
||||
"PURCHASE_ERROR": "Failed to process purchase. Please try again.",
|
||||
"PURCHASE_SUCCESS": "Successfully added {credits} credits to your account",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Purchase",
|
||||
"DESCRIPTION": "You are about to purchase {credits} credits for {amount}.",
|
||||
"INSTANT_DEDUCTION_NOTE": "Your saved card will be charged immediately upon confirmation.",
|
||||
"GO_BACK": "العودة للخلف",
|
||||
"CONFIRM_PURCHASE": "Confirm Purchase"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
"TITLE": "Security",
|
||||
"DESCRIPTION": "Manage your account security settings.",
|
||||
"LINK_TEXT": "Learn more about SAML SSO",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"YEAR_IN_REVIEW": {
|
||||
"TITLE": "Year in Review",
|
||||
"LOADING": "Loading your year in review...",
|
||||
"ERROR": "Failed to load year in review",
|
||||
"CLOSE": "أغلق",
|
||||
"CONVERSATIONS": {
|
||||
"TITLE": "You have handled",
|
||||
"SUBTITLE": "المحادثات",
|
||||
"FALLBACK": "This year wasn't about the numbers. It was about showing up.",
|
||||
"COMPARISON": {
|
||||
"0_50": "You showed up, and that's how every good inbox begins.",
|
||||
"50_100": "You kept the replies flowing and the conversations alive.",
|
||||
"100_500": "You handled serious volume and kept everything on track.",
|
||||
"500_2000": "You kept things moving while the volume kept climbing.",
|
||||
"2000_10000": "You ran high traffic through your inbox without breaking a sweat.",
|
||||
"10000_PLUS": "That's a full city of customers knocking on your door. You made it look effortless."
|
||||
}
|
||||
},
|
||||
"BUSIEST_DAY": {
|
||||
"TITLE": "Your busiest day was",
|
||||
"MESSAGE": "{count} conversations that day.",
|
||||
"COMPARISON": {
|
||||
"0_5": "A warm-up lap that barely woke the inbox.",
|
||||
"5_10": "Enough action to justify a second cup of coffee.",
|
||||
"10_25": "Things got busy and the inbox stayed on its toes.",
|
||||
"25_50": "A proper rush that barely broke a sweat.",
|
||||
"50_100": "Controlled chaos, handled like a normal Tuesday.",
|
||||
"100_500": "Absolute dumpster fire, somehow still shipping replies.",
|
||||
"500_PLUS": "The inbox lost all chill and never slowed down."
|
||||
}
|
||||
},
|
||||
"PERSONALITY": {
|
||||
"TITLE": "Your support personality is",
|
||||
"MESSAGES": {
|
||||
"SWIFT_HELPER": "You replied in {time} on average. Faster than most notifications.",
|
||||
"QUICK_RESPONDER": "You replied in {time} on average. The inbox barely waited.",
|
||||
"STEADY_SUPPORT": "You replied in {time} on average. Calm pace, solid replies.",
|
||||
"THOUGHTFUL_ADVISOR": "You replied in {time} on average. Took the time to get it right."
|
||||
}
|
||||
},
|
||||
"THANK_YOU": {
|
||||
"TITLE": "Congratulations on surviving the inbox of {year}.",
|
||||
"MESSAGE": "Thank you for your incredible dedication to supporting customers throughout this year. Your hard work has made a real difference, and we're grateful to have you on this journey. Here's to making {nextYear} even better together!"
|
||||
},
|
||||
"SHARE_MODAL": {
|
||||
"TITLE": "Share Your Year in Review",
|
||||
"PREPARING": "Preparing your image...",
|
||||
"DOWNLOAD": "تحميل",
|
||||
"SHARE_TITLE": "My {year} Year in Review",
|
||||
"SHARE_TEXT": "Check out my {year} Year in Review with Chatwoot!",
|
||||
"BRANDING": "Made with Chatwoot"
|
||||
},
|
||||
"BANNER": {
|
||||
"TITLE": "Your {year} Year in Review is here",
|
||||
"BUTTON": "See your impact"
|
||||
},
|
||||
"NAVIGATION": {
|
||||
"PREVIOUS": "Previous",
|
||||
"NEXT": "التالي",
|
||||
"SHARE": "مشاركة"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,10 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"COMPANIES": {
|
||||
"HEADER": "Companies",
|
||||
"SORT_BY": {
|
||||
"LABEL": "Sort by",
|
||||
"OPTIONS": {
|
||||
"NAME": "Name",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Created at"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
"LABEL": "Order",
|
||||
"OPTIONS": {
|
||||
"ASCENDING": "Ascending",
|
||||
"DESCENDING": "Descending"
|
||||
}
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Search companies...",
|
||||
"LOADING": "Loading companies...",
|
||||
"UNNAMED": "Unnamed Company",
|
||||
"CONTACTS_COUNT": "{n} contact | {n} contacts",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No companies found"
|
||||
}
|
||||
},
|
||||
"COMPANIES_LAYOUT": {
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "Call",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -456,6 +457,9 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"CANNOT_REPLY": "You cannot reply due to",
|
||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"48_HOURS_WINDOW": "48 hour message window restriction",
|
||||
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
||||
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
||||
"ASSIGN_TO_ME": "Assign to me",
|
||||
@@ -196,7 +197,6 @@
|
||||
"INSERT_READ_MORE": "Read more",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"TIP_AUDIORECORDER_ICON": "Record audio",
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
|
||||
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You won’t be able to send/receive Instagram messages from this inbox anymore."
|
||||
},
|
||||
"TIKTOK": {
|
||||
"CONTINUE_WITH_TIKTOK": "Continue with TikTok",
|
||||
"CONNECT_YOUR_TIKTOK_PROFILE": "Connect your TikTok Profile",
|
||||
"HELP": "To add your TikTok profile as a channel, you need to authenticate your TikTok Profile by clicking on 'Continue with TikTok' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to TikTok, please try again",
|
||||
"ERROR_AUTH": "There was an error connecting to TikTok, please try again"
|
||||
},
|
||||
"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",
|
||||
@@ -381,7 +388,11 @@
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the email channel"
|
||||
},
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address.",
|
||||
"FINISH_MESSAGE_NO_FORWARDING": "Your email inbox has been created successfully! You need to configure SMTP and IMAP credentials to send and receive emails. Without these settings, no emails will be processed.",
|
||||
"FORWARDING_ADDRESS_LABEL": "Forward emails to this address:",
|
||||
"CONFIGURE_SMTP_IMAP_LINK": "Click here",
|
||||
"CONFIGURE_SMTP_IMAP_TEXT": " to configure IMAP and SMTP settings"
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "LINE Channel",
|
||||
@@ -467,6 +478,10 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -707,6 +722,7 @@
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.",
|
||||
"FORWARD_EMAIL_NOT_CONFIGURED": "Forwarding emails to your inbox is currently disabled on this installation. To use this feature, it must be enabled by your administrator. Please get in touch with them to proceed.",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved.",
|
||||
"WHATSAPP_SECTION_SUBHEADER": "This API Key is used for the integration with the WhatsApp APIs.",
|
||||
@@ -1004,6 +1020,7 @@
|
||||
"LINE": "Line",
|
||||
"API": "API Channel",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"CONTACT_SUPPORT": "Contact support",
|
||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||
"PROFILE_SETTINGS": "Profile settings",
|
||||
"YEAR_IN_REVIEW": "Year in Review",
|
||||
"KEYBOARD_SHORTCUTS": "Keyboard shortcuts",
|
||||
"APPEARANCE": "Change appearance",
|
||||
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
|
||||
@@ -306,6 +307,8 @@
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Contacts",
|
||||
"ACTIVE": "Active",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -398,19 +401,44 @@
|
||||
"BUTTON_TXT": "Buy more credits",
|
||||
"DOCUMENTS": "Documents",
|
||||
"RESPONSES": "Responses",
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more.",
|
||||
"REFRESH_CREDITS": "Refresh"
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "Need help?",
|
||||
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
|
||||
"BUTTON_TXT": "Chat with us"
|
||||
},
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again.",
|
||||
"TOPUP": {
|
||||
"BUY_CREDITS": "Buy more credits",
|
||||
"MODAL_TITLE": "Buy AI Credits",
|
||||
"MODAL_DESCRIPTION": "Purchase additional credits for Captain AI.",
|
||||
"CREDITS": "CREDITS",
|
||||
"ONE_TIME": "one-time",
|
||||
"POPULAR": "Most Popular",
|
||||
"NOTE_TITLE": "Note:",
|
||||
"NOTE_DESCRIPTION": "Credits are added immediately and expire in 6 months. An active subscription is required to use credits. Purchased credits are consumed after your monthly plan credits.",
|
||||
"CANCEL": "Cancel",
|
||||
"PURCHASE": "Purchase Credits",
|
||||
"LOADING": "Loading options...",
|
||||
"FETCH_ERROR": "Failed to load credit options. Please try again.",
|
||||
"PURCHASE_ERROR": "Failed to process purchase. Please try again.",
|
||||
"PURCHASE_SUCCESS": "Successfully added {credits} credits to your account",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Purchase",
|
||||
"DESCRIPTION": "You are about to purchase {credits} credits for {amount}.",
|
||||
"INSTANT_DEDUCTION_NOTE": "Your saved card will be charged immediately upon confirmation.",
|
||||
"GO_BACK": "Go Back",
|
||||
"CONFIRM_PURCHASE": "Confirm Purchase"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
"TITLE": "Security",
|
||||
"DESCRIPTION": "Manage your account security settings.",
|
||||
"LINK_TEXT": "Learn more about SAML SSO",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"YEAR_IN_REVIEW": {
|
||||
"TITLE": "Year in Review",
|
||||
"LOADING": "Loading your year in review...",
|
||||
"ERROR": "Failed to load year in review",
|
||||
"CLOSE": "Close",
|
||||
"CONVERSATIONS": {
|
||||
"TITLE": "You have handled",
|
||||
"SUBTITLE": "conversations",
|
||||
"FALLBACK": "This year wasn't about the numbers. It was about showing up.",
|
||||
"COMPARISON": {
|
||||
"0_50": "You showed up, and that's how every good inbox begins.",
|
||||
"50_100": "You kept the replies flowing and the conversations alive.",
|
||||
"100_500": "You handled serious volume and kept everything on track.",
|
||||
"500_2000": "You kept things moving while the volume kept climbing.",
|
||||
"2000_10000": "You ran high traffic through your inbox without breaking a sweat.",
|
||||
"10000_PLUS": "That's a full city of customers knocking on your door. You made it look effortless."
|
||||
}
|
||||
},
|
||||
"BUSIEST_DAY": {
|
||||
"TITLE": "Your busiest day was",
|
||||
"MESSAGE": "{count} conversations that day.",
|
||||
"COMPARISON": {
|
||||
"0_5": "A warm-up lap that barely woke the inbox.",
|
||||
"5_10": "Enough action to justify a second cup of coffee.",
|
||||
"10_25": "Things got busy and the inbox stayed on its toes.",
|
||||
"25_50": "A proper rush that barely broke a sweat.",
|
||||
"50_100": "Controlled chaos, handled like a normal Tuesday.",
|
||||
"100_500": "Absolute dumpster fire, somehow still shipping replies.",
|
||||
"500_PLUS": "The inbox lost all chill and never slowed down."
|
||||
}
|
||||
},
|
||||
"PERSONALITY": {
|
||||
"TITLE": "Your support personality is",
|
||||
"MESSAGES": {
|
||||
"SWIFT_HELPER": "You replied in {time} on average. Faster than most notifications.",
|
||||
"QUICK_RESPONDER": "You replied in {time} on average. The inbox barely waited.",
|
||||
"STEADY_SUPPORT": "You replied in {time} on average. Calm pace, solid replies.",
|
||||
"THOUGHTFUL_ADVISOR": "You replied in {time} on average. Took the time to get it right."
|
||||
}
|
||||
},
|
||||
"THANK_YOU": {
|
||||
"TITLE": "Congratulations on surviving the inbox of {year}.",
|
||||
"MESSAGE": "Thank you for your incredible dedication to supporting customers throughout this year. Your hard work has made a real difference, and we're grateful to have you on this journey. Here's to making {nextYear} even better together!"
|
||||
},
|
||||
"SHARE_MODAL": {
|
||||
"TITLE": "Share Your Year in Review",
|
||||
"PREPARING": "Preparing your image...",
|
||||
"DOWNLOAD": "Download",
|
||||
"SHARE_TITLE": "My {year} Year in Review",
|
||||
"SHARE_TEXT": "Check out my {year} Year in Review with Chatwoot!",
|
||||
"BRANDING": "Made with Chatwoot"
|
||||
},
|
||||
"BANNER": {
|
||||
"TITLE": "Your {year} Year in Review is here",
|
||||
"BUTTON": "See your impact"
|
||||
},
|
||||
"NAVIGATION": {
|
||||
"PREVIOUS": "Previous",
|
||||
"NEXT": "Next",
|
||||
"SHARE": "Share"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,10 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"COMPANIES": {
|
||||
"HEADER": "Companies",
|
||||
"SORT_BY": {
|
||||
"LABEL": "Sort by",
|
||||
"OPTIONS": {
|
||||
"NAME": "Име",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Създаден в"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
"LABEL": "Order",
|
||||
"OPTIONS": {
|
||||
"ASCENDING": "Ascending",
|
||||
"DESCENDING": "Descending"
|
||||
}
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Search companies...",
|
||||
"LOADING": "Loading companies...",
|
||||
"UNNAMED": "Unnamed Company",
|
||||
"CONTACTS_COUNT": "{n} contact | {n} contacts",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No companies found"
|
||||
}
|
||||
},
|
||||
"COMPANIES_LAYOUT": {
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "Ново съобщение",
|
||||
"CALL": "Call",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -456,6 +457,9 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"CANNOT_REPLY": "You cannot reply due to",
|
||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"48_HOURS_WINDOW": "48 hour message window restriction",
|
||||
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
||||
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
||||
"ASSIGN_TO_ME": "Assign to me",
|
||||
@@ -196,7 +197,6 @@
|
||||
"INSERT_READ_MORE": "Read more",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"TIP_AUDIORECORDER_ICON": "Record audio",
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
|
||||
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You won’t be able to send/receive Instagram messages from this inbox anymore."
|
||||
},
|
||||
"TIKTOK": {
|
||||
"CONTINUE_WITH_TIKTOK": "Continue with TikTok",
|
||||
"CONNECT_YOUR_TIKTOK_PROFILE": "Connect your TikTok Profile",
|
||||
"HELP": "To add your TikTok profile as a channel, you need to authenticate your TikTok Profile by clicking on 'Continue with TikTok' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to TikTok, please try again",
|
||||
"ERROR_AUTH": "There was an error connecting to TikTok, please try again"
|
||||
},
|
||||
"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",
|
||||
@@ -381,7 +388,11 @@
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the email channel"
|
||||
},
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address.",
|
||||
"FINISH_MESSAGE_NO_FORWARDING": "Your email inbox has been created successfully! You need to configure SMTP and IMAP credentials to send and receive emails. Without these settings, no emails will be processed.",
|
||||
"FORWARDING_ADDRESS_LABEL": "Forward emails to this address:",
|
||||
"CONFIGURE_SMTP_IMAP_LINK": "Click here",
|
||||
"CONFIGURE_SMTP_IMAP_TEXT": " to configure IMAP and SMTP settings"
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "LINE Channel",
|
||||
@@ -467,6 +478,10 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -707,6 +722,7 @@
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.",
|
||||
"FORWARD_EMAIL_NOT_CONFIGURED": "Forwarding emails to your inbox is currently disabled on this installation. To use this feature, it must be enabled by your administrator. Please get in touch with them to proceed.",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved.",
|
||||
"WHATSAPP_SECTION_SUBHEADER": "This API Key is used for the integration with the WhatsApp APIs.",
|
||||
@@ -1004,6 +1020,7 @@
|
||||
"LINE": "Line",
|
||||
"API": "API Channel",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"CONTACT_SUPPORT": "Contact support",
|
||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||
"PROFILE_SETTINGS": "Profile settings",
|
||||
"YEAR_IN_REVIEW": "Year in Review",
|
||||
"KEYBOARD_SHORTCUTS": "Keyboard shortcuts",
|
||||
"APPEARANCE": "Change appearance",
|
||||
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
|
||||
@@ -306,6 +307,8 @@
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Контакти",
|
||||
"ACTIVE": "Активен",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -398,19 +401,44 @@
|
||||
"BUTTON_TXT": "Buy more credits",
|
||||
"DOCUMENTS": "Documents",
|
||||
"RESPONSES": "Responses",
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more.",
|
||||
"REFRESH_CREDITS": "Refresh"
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "Need help?",
|
||||
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
|
||||
"BUTTON_TXT": "Чатете с нас"
|
||||
},
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again.",
|
||||
"TOPUP": {
|
||||
"BUY_CREDITS": "Buy more credits",
|
||||
"MODAL_TITLE": "Buy AI Credits",
|
||||
"MODAL_DESCRIPTION": "Purchase additional credits for Captain AI.",
|
||||
"CREDITS": "CREDITS",
|
||||
"ONE_TIME": "one-time",
|
||||
"POPULAR": "Most Popular",
|
||||
"NOTE_TITLE": "Note:",
|
||||
"NOTE_DESCRIPTION": "Credits are added immediately and expire in 6 months. An active subscription is required to use credits. Purchased credits are consumed after your monthly plan credits.",
|
||||
"CANCEL": "Отмени",
|
||||
"PURCHASE": "Purchase Credits",
|
||||
"LOADING": "Loading options...",
|
||||
"FETCH_ERROR": "Failed to load credit options. Please try again.",
|
||||
"PURCHASE_ERROR": "Failed to process purchase. Please try again.",
|
||||
"PURCHASE_SUCCESS": "Successfully added {credits} credits to your account",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Purchase",
|
||||
"DESCRIPTION": "You are about to purchase {credits} credits for {amount}.",
|
||||
"INSTANT_DEDUCTION_NOTE": "Your saved card will be charged immediately upon confirmation.",
|
||||
"GO_BACK": "Go Back",
|
||||
"CONFIRM_PURCHASE": "Confirm Purchase"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
"TITLE": "Security",
|
||||
"DESCRIPTION": "Manage your account security settings.",
|
||||
"LINK_TEXT": "Learn more about SAML SSO",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"YEAR_IN_REVIEW": {
|
||||
"TITLE": "Year in Review",
|
||||
"LOADING": "Loading your year in review...",
|
||||
"ERROR": "Failed to load year in review",
|
||||
"CLOSE": "Close",
|
||||
"CONVERSATIONS": {
|
||||
"TITLE": "You have handled",
|
||||
"SUBTITLE": "разговори",
|
||||
"FALLBACK": "This year wasn't about the numbers. It was about showing up.",
|
||||
"COMPARISON": {
|
||||
"0_50": "You showed up, and that's how every good inbox begins.",
|
||||
"50_100": "You kept the replies flowing and the conversations alive.",
|
||||
"100_500": "You handled serious volume and kept everything on track.",
|
||||
"500_2000": "You kept things moving while the volume kept climbing.",
|
||||
"2000_10000": "You ran high traffic through your inbox without breaking a sweat.",
|
||||
"10000_PLUS": "That's a full city of customers knocking on your door. You made it look effortless."
|
||||
}
|
||||
},
|
||||
"BUSIEST_DAY": {
|
||||
"TITLE": "Your busiest day was",
|
||||
"MESSAGE": "{count} conversations that day.",
|
||||
"COMPARISON": {
|
||||
"0_5": "A warm-up lap that barely woke the inbox.",
|
||||
"5_10": "Enough action to justify a second cup of coffee.",
|
||||
"10_25": "Things got busy and the inbox stayed on its toes.",
|
||||
"25_50": "A proper rush that barely broke a sweat.",
|
||||
"50_100": "Controlled chaos, handled like a normal Tuesday.",
|
||||
"100_500": "Absolute dumpster fire, somehow still shipping replies.",
|
||||
"500_PLUS": "The inbox lost all chill and never slowed down."
|
||||
}
|
||||
},
|
||||
"PERSONALITY": {
|
||||
"TITLE": "Your support personality is",
|
||||
"MESSAGES": {
|
||||
"SWIFT_HELPER": "You replied in {time} on average. Faster than most notifications.",
|
||||
"QUICK_RESPONDER": "You replied in {time} on average. The inbox barely waited.",
|
||||
"STEADY_SUPPORT": "You replied in {time} on average. Calm pace, solid replies.",
|
||||
"THOUGHTFUL_ADVISOR": "You replied in {time} on average. Took the time to get it right."
|
||||
}
|
||||
},
|
||||
"THANK_YOU": {
|
||||
"TITLE": "Congratulations on surviving the inbox of {year}.",
|
||||
"MESSAGE": "Thank you for your incredible dedication to supporting customers throughout this year. Your hard work has made a real difference, and we're grateful to have you on this journey. Here's to making {nextYear} even better together!"
|
||||
},
|
||||
"SHARE_MODAL": {
|
||||
"TITLE": "Share Your Year in Review",
|
||||
"PREPARING": "Preparing your image...",
|
||||
"DOWNLOAD": "Download",
|
||||
"SHARE_TITLE": "My {year} Year in Review",
|
||||
"SHARE_TEXT": "Check out my {year} Year in Review with Chatwoot!",
|
||||
"BRANDING": "Made with Chatwoot"
|
||||
},
|
||||
"BANNER": {
|
||||
"TITLE": "Your {year} Year in Review is here",
|
||||
"BUTTON": "See your impact"
|
||||
},
|
||||
"NAVIGATION": {
|
||||
"PREVIOUS": "Previous",
|
||||
"NEXT": "Next",
|
||||
"SHARE": "Share"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,10 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"COMPANIES": {
|
||||
"HEADER": "Companies",
|
||||
"SORT_BY": {
|
||||
"LABEL": "Sort by",
|
||||
"OPTIONS": {
|
||||
"NAME": "Name",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Created at"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
"LABEL": "Order",
|
||||
"OPTIONS": {
|
||||
"ASCENDING": "Ascending",
|
||||
"DESCENDING": "Descending"
|
||||
}
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Search companies...",
|
||||
"LOADING": "Loading companies...",
|
||||
"UNNAMED": "Unnamed Company",
|
||||
"CONTACTS_COUNT": "{n} contact | {n} contacts",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No companies found"
|
||||
}
|
||||
},
|
||||
"COMPANIES_LAYOUT": {
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "Call",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -456,6 +457,9 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"LOADING_CONVERSATIONS": "Loading Conversations",
|
||||
"CANNOT_REPLY": "You cannot reply due to",
|
||||
"24_HOURS_WINDOW": "24 hour message window restriction",
|
||||
"48_HOURS_WINDOW": "48 hour message window restriction",
|
||||
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
||||
"NOT_ASSIGNED_TO_YOU": "This conversation is not assigned to you. Would you like to assign this conversation to yourself?",
|
||||
"ASSIGN_TO_ME": "Assign to me",
|
||||
@@ -196,7 +197,6 @@
|
||||
"INSERT_READ_MORE": "Read more",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"TIP_FORMAT_ICON": "Show rich text editor",
|
||||
"TIP_EMOJI_ICON": "Show emoji selector",
|
||||
"TIP_ATTACH_ICON": "Attach files",
|
||||
"TIP_AUDIORECORDER_ICON": "Record audio",
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
|
||||
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You won’t be able to send/receive Instagram messages from this inbox anymore."
|
||||
},
|
||||
"TIKTOK": {
|
||||
"CONTINUE_WITH_TIKTOK": "Continue with TikTok",
|
||||
"CONNECT_YOUR_TIKTOK_PROFILE": "Connect your TikTok Profile",
|
||||
"HELP": "To add your TikTok profile as a channel, you need to authenticate your TikTok Profile by clicking on 'Continue with TikTok' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to TikTok, please try again",
|
||||
"ERROR_AUTH": "There was an error connecting to TikTok, please try again"
|
||||
},
|
||||
"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",
|
||||
@@ -381,7 +388,11 @@
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "We were not able to save the email channel"
|
||||
},
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address.",
|
||||
"FINISH_MESSAGE_NO_FORWARDING": "Your email inbox has been created successfully! You need to configure SMTP and IMAP credentials to send and receive emails. Without these settings, no emails will be processed.",
|
||||
"FORWARDING_ADDRESS_LABEL": "Forward emails to this address:",
|
||||
"CONFIGURE_SMTP_IMAP_LINK": "Click here",
|
||||
"CONFIGURE_SMTP_IMAP_TEXT": " to configure IMAP and SMTP settings"
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "LINE Channel",
|
||||
@@ -467,6 +478,10 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -707,6 +722,7 @@
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Use the `inbox_identifier` token shown here to authentication your API clients.",
|
||||
"FORWARD_EMAIL_TITLE": "Forward to Email",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "Start forwarding your emails to the following email address.",
|
||||
"FORWARD_EMAIL_NOT_CONFIGURED": "Forwarding emails to your inbox is currently disabled on this installation. To use this feature, it must be enabled by your administrator. Please get in touch with them to proceed.",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "Allow messages after conversation resolved",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Allow the end-users to send messages even after the conversation is resolved.",
|
||||
"WHATSAPP_SECTION_SUBHEADER": "This API Key is used for the integration with the WhatsApp APIs.",
|
||||
@@ -1004,6 +1020,7 @@
|
||||
"LINE": "Line",
|
||||
"API": "API Channel",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"CONTACT_SUPPORT": "Contact support",
|
||||
"SELECTOR_SUBTITLE": "Select an account from the following list",
|
||||
"PROFILE_SETTINGS": "Profile settings",
|
||||
"YEAR_IN_REVIEW": "Year in Review",
|
||||
"KEYBOARD_SHORTCUTS": "Keyboard shortcuts",
|
||||
"APPEARANCE": "Change appearance",
|
||||
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
|
||||
@@ -306,6 +307,8 @@
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Contacts",
|
||||
"ACTIVE": "Active",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -398,19 +401,44 @@
|
||||
"BUTTON_TXT": "Buy more credits",
|
||||
"DOCUMENTS": "Documents",
|
||||
"RESPONSES": "Responses",
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more.",
|
||||
"REFRESH_CREDITS": "Refresh"
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "Need help?",
|
||||
"DESCRIPTION": "Do you face any issues in billing? We are here to help.",
|
||||
"BUTTON_TXT": "Chat with us"
|
||||
},
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again.",
|
||||
"TOPUP": {
|
||||
"BUY_CREDITS": "Buy more credits",
|
||||
"MODAL_TITLE": "Buy AI Credits",
|
||||
"MODAL_DESCRIPTION": "Purchase additional credits for Captain AI.",
|
||||
"CREDITS": "CREDITS",
|
||||
"ONE_TIME": "one-time",
|
||||
"POPULAR": "Most Popular",
|
||||
"NOTE_TITLE": "Note:",
|
||||
"NOTE_DESCRIPTION": "Credits are added immediately and expire in 6 months. An active subscription is required to use credits. Purchased credits are consumed after your monthly plan credits.",
|
||||
"CANCEL": "Cancel",
|
||||
"PURCHASE": "Purchase Credits",
|
||||
"LOADING": "Loading options...",
|
||||
"FETCH_ERROR": "Failed to load credit options. Please try again.",
|
||||
"PURCHASE_ERROR": "Failed to process purchase. Please try again.",
|
||||
"PURCHASE_SUCCESS": "Successfully added {credits} credits to your account",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Purchase",
|
||||
"DESCRIPTION": "You are about to purchase {credits} credits for {amount}.",
|
||||
"INSTANT_DEDUCTION_NOTE": "Your saved card will be charged immediately upon confirmation.",
|
||||
"GO_BACK": "Go Back",
|
||||
"CONFIRM_PURCHASE": "Confirm Purchase"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
"TITLE": "Security",
|
||||
"DESCRIPTION": "Manage your account security settings.",
|
||||
"LINK_TEXT": "Learn more about SAML SSO",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"YEAR_IN_REVIEW": {
|
||||
"TITLE": "Year in Review",
|
||||
"LOADING": "Loading your year in review...",
|
||||
"ERROR": "Failed to load year in review",
|
||||
"CLOSE": "Close",
|
||||
"CONVERSATIONS": {
|
||||
"TITLE": "You have handled",
|
||||
"SUBTITLE": "conversations",
|
||||
"FALLBACK": "This year wasn't about the numbers. It was about showing up.",
|
||||
"COMPARISON": {
|
||||
"0_50": "You showed up, and that's how every good inbox begins.",
|
||||
"50_100": "You kept the replies flowing and the conversations alive.",
|
||||
"100_500": "You handled serious volume and kept everything on track.",
|
||||
"500_2000": "You kept things moving while the volume kept climbing.",
|
||||
"2000_10000": "You ran high traffic through your inbox without breaking a sweat.",
|
||||
"10000_PLUS": "That's a full city of customers knocking on your door. You made it look effortless."
|
||||
}
|
||||
},
|
||||
"BUSIEST_DAY": {
|
||||
"TITLE": "Your busiest day was",
|
||||
"MESSAGE": "{count} conversations that day.",
|
||||
"COMPARISON": {
|
||||
"0_5": "A warm-up lap that barely woke the inbox.",
|
||||
"5_10": "Enough action to justify a second cup of coffee.",
|
||||
"10_25": "Things got busy and the inbox stayed on its toes.",
|
||||
"25_50": "A proper rush that barely broke a sweat.",
|
||||
"50_100": "Controlled chaos, handled like a normal Tuesday.",
|
||||
"100_500": "Absolute dumpster fire, somehow still shipping replies.",
|
||||
"500_PLUS": "The inbox lost all chill and never slowed down."
|
||||
}
|
||||
},
|
||||
"PERSONALITY": {
|
||||
"TITLE": "Your support personality is",
|
||||
"MESSAGES": {
|
||||
"SWIFT_HELPER": "You replied in {time} on average. Faster than most notifications.",
|
||||
"QUICK_RESPONDER": "You replied in {time} on average. The inbox barely waited.",
|
||||
"STEADY_SUPPORT": "You replied in {time} on average. Calm pace, solid replies.",
|
||||
"THOUGHTFUL_ADVISOR": "You replied in {time} on average. Took the time to get it right."
|
||||
}
|
||||
},
|
||||
"THANK_YOU": {
|
||||
"TITLE": "Congratulations on surviving the inbox of {year}.",
|
||||
"MESSAGE": "Thank you for your incredible dedication to supporting customers throughout this year. Your hard work has made a real difference, and we're grateful to have you on this journey. Here's to making {nextYear} even better together!"
|
||||
},
|
||||
"SHARE_MODAL": {
|
||||
"TITLE": "Share Your Year in Review",
|
||||
"PREPARING": "Preparing your image...",
|
||||
"DOWNLOAD": "Download",
|
||||
"SHARE_TITLE": "My {year} Year in Review",
|
||||
"SHARE_TEXT": "Check out my {year} Year in Review with Chatwoot!",
|
||||
"BRANDING": "Made with Chatwoot"
|
||||
},
|
||||
"BANNER": {
|
||||
"TITLE": "Your {year} Year in Review is here",
|
||||
"BUTTON": "See your impact"
|
||||
},
|
||||
"NAVIGATION": {
|
||||
"PREVIOUS": "Previous",
|
||||
"NEXT": "Next",
|
||||
"SHARE": "Share"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,10 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Activa la validació d'expressions regulars"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"COMPANIES": {
|
||||
"HEADER": "Companies",
|
||||
"SORT_BY": {
|
||||
"LABEL": "Ordenat per",
|
||||
"OPTIONS": {
|
||||
"NAME": "Nom",
|
||||
"DOMAIN": "Domini",
|
||||
"CREATED_AT": "Creat per"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
"LABEL": "Order",
|
||||
"OPTIONS": {
|
||||
"ASCENDING": "Ascending",
|
||||
"DESCENDING": "Descending"
|
||||
}
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Search companies...",
|
||||
"LOADING": "Loading companies...",
|
||||
"UNNAMED": "Unnamed Company",
|
||||
"CONTACTS_COUNT": "{n} contact | {n} contacts",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No companies found"
|
||||
}
|
||||
},
|
||||
"COMPANIES_LAYOUT": {
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@
|
||||
"CREATED_AT_LABEL": "Creat",
|
||||
"NEW_MESSAGE": "Nou missatge",
|
||||
"CALL": "Call",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -456,6 +457,9 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"LOADING_CONVERSATIONS": "S'estan carregant les converses",
|
||||
"CANNOT_REPLY": "No pots respondre degut a",
|
||||
"24_HOURS_WINDOW": "Restricció de finestra de missatges de 24 hores",
|
||||
"48_HOURS_WINDOW": "Restricció de finestra de missatges de 48 hores",
|
||||
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
||||
"NOT_ASSIGNED_TO_YOU": "Aquesta conversa no està assignada a tu. Vols assignar-te-la?",
|
||||
"ASSIGN_TO_ME": "Assigna'm",
|
||||
@@ -196,7 +197,6 @@
|
||||
"INSERT_READ_MORE": "Llegir més",
|
||||
"DISMISS_REPLY": "Ignora la resposta",
|
||||
"REPLYING_TO": "Responent a:",
|
||||
"TIP_FORMAT_ICON": "Mostra l'editor de text enriquit",
|
||||
"TIP_EMOJI_ICON": "Mostra la selecció d'emoticones",
|
||||
"TIP_ATTACH_ICON": "Ajuntar fitxers",
|
||||
"TIP_AUDIORECORDER_ICON": "Gravar àudio",
|
||||
|
||||
@@ -57,6 +57,13 @@
|
||||
"NEW_INBOX_SUGGESTION": "This Instagram account was previously linked to a different inbox and has now been migrated here. All new messages will appear here. The old inbox will no longer be able to send or receive messages for this account.",
|
||||
"DUPLICATE_INBOX_BANNER": "This Instagram account was migrated to the new Instagram channel inbox. You won’t be able to send/receive Instagram messages from this inbox anymore."
|
||||
},
|
||||
"TIKTOK": {
|
||||
"CONTINUE_WITH_TIKTOK": "Continue with TikTok",
|
||||
"CONNECT_YOUR_TIKTOK_PROFILE": "Connect your TikTok Profile",
|
||||
"HELP": "To add your TikTok profile as a channel, you need to authenticate your TikTok Profile by clicking on 'Continue with TikTok' ",
|
||||
"ERROR_MESSAGE": "There was an error connecting to TikTok, please try again",
|
||||
"ERROR_AUTH": "There was an error connecting to TikTok, please try again"
|
||||
},
|
||||
"TWITTER": {
|
||||
"HELP": "Per afegir el teu perfil de Twitter com a canal, has d'autentificar el vostre perfil de Twitter fent clic a 'Inicieu la sessió amb Twitter' ",
|
||||
"ERROR_MESSAGE": "S'ha produït un error en connectar amb Twitter, torna-ho a provar",
|
||||
@@ -381,7 +388,11 @@
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "No hem pogut desar el canal de correu electrònic"
|
||||
},
|
||||
"FINISH_MESSAGE": "Comença a reenviar els teus correus electrònics a la següent adreça electrònica."
|
||||
"FINISH_MESSAGE": "Comença a reenviar els teus correus electrònics a la següent adreça electrònica.",
|
||||
"FINISH_MESSAGE_NO_FORWARDING": "Your email inbox has been created successfully! You need to configure SMTP and IMAP credentials to send and receive emails. Without these settings, no emails will be processed.",
|
||||
"FORWARDING_ADDRESS_LABEL": "Forward emails to this address:",
|
||||
"CONFIGURE_SMTP_IMAP_LINK": "Clica aquí",
|
||||
"CONFIGURE_SMTP_IMAP_TEXT": " to configure IMAP and SMTP settings"
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "Canal LINE",
|
||||
@@ -467,6 +478,10 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -707,6 +722,7 @@
|
||||
"INBOX_IDENTIFIER_SUB_TEXT": "Utilitza el token \"inbox_identifier\" que es mostra aquí per autenticar els vostres clients de l'API.",
|
||||
"FORWARD_EMAIL_TITLE": "Reenvia al correu electrònic",
|
||||
"FORWARD_EMAIL_SUB_TEXT": "Comença a reenviar els teus correus electrònics a la següent adreça electrònica.",
|
||||
"FORWARD_EMAIL_NOT_CONFIGURED": "Forwarding emails to your inbox is currently disabled on this installation. To use this feature, it must be enabled by your administrator. Please get in touch with them to proceed.",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED": "Permet missatges després de resoldre la conversa",
|
||||
"ALLOW_MESSAGES_AFTER_RESOLVED_SUB_TEXT": "Permet als usuaris finals enviar missatges fins i tot després de resoldre la conversa.",
|
||||
"WHATSAPP_SECTION_SUBHEADER": "Aquesta API key s'utilitza per a la integració amb les APIs de WhatsApp.",
|
||||
@@ -1004,6 +1020,7 @@
|
||||
"LINE": "Line",
|
||||
"API": "Canal de l'API",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,7 @@
|
||||
"CONTACT_SUPPORT": "Contact support",
|
||||
"SELECTOR_SUBTITLE": "Selecciona un compte de la llista següent",
|
||||
"PROFILE_SETTINGS": "Profile settings",
|
||||
"YEAR_IN_REVIEW": "Year in Review",
|
||||
"KEYBOARD_SHORTCUTS": "Keyboard shortcuts",
|
||||
"APPEARANCE": "Change appearance",
|
||||
"SUPER_ADMIN_CONSOLE": "SuperAdmin console",
|
||||
@@ -306,6 +307,8 @@
|
||||
"SETTINGS": "Configuracions",
|
||||
"CONTACTS": "Contactes",
|
||||
"ACTIVE": "Actiu",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -398,19 +401,44 @@
|
||||
"BUTTON_TXT": "Buy more credits",
|
||||
"DOCUMENTS": "Documents",
|
||||
"RESPONSES": "Responses",
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more.",
|
||||
"REFRESH_CREDITS": "Actualitza"
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "Necessita ajuda?",
|
||||
"DESCRIPTION": "Trobes algun problema en la facturació? Estem aquí per ajudar.",
|
||||
"BUTTON_TXT": "Xateja amb nosaltres"
|
||||
},
|
||||
"NO_BILLING_USER": "El teu compte de facturació s'està configurant. Actualitza la pàgina i torna-ho a provar."
|
||||
"NO_BILLING_USER": "El teu compte de facturació s'està configurant. Actualitza la pàgina i torna-ho a provar.",
|
||||
"TOPUP": {
|
||||
"BUY_CREDITS": "Buy more credits",
|
||||
"MODAL_TITLE": "Buy AI Credits",
|
||||
"MODAL_DESCRIPTION": "Purchase additional credits for Captain AI.",
|
||||
"CREDITS": "CREDITS",
|
||||
"ONE_TIME": "one-time",
|
||||
"POPULAR": "Most Popular",
|
||||
"NOTE_TITLE": "Nota:",
|
||||
"NOTE_DESCRIPTION": "Credits are added immediately and expire in 6 months. An active subscription is required to use credits. Purchased credits are consumed after your monthly plan credits.",
|
||||
"CANCEL": "Cancel·la",
|
||||
"PURCHASE": "Purchase Credits",
|
||||
"LOADING": "Loading options...",
|
||||
"FETCH_ERROR": "Failed to load credit options. Please try again.",
|
||||
"PURCHASE_ERROR": "Failed to process purchase. Please try again.",
|
||||
"PURCHASE_SUCCESS": "Successfully added {credits} credits to your account",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirm Purchase",
|
||||
"DESCRIPTION": "You are about to purchase {credits} credits for {amount}.",
|
||||
"INSTANT_DEDUCTION_NOTE": "Your saved card will be charged immediately upon confirmation.",
|
||||
"GO_BACK": "Torna enrere",
|
||||
"CONFIRM_PURCHASE": "Confirm Purchase"
|
||||
}
|
||||
}
|
||||
},
|
||||
"SECURITY_SETTINGS": {
|
||||
"TITLE": "Security",
|
||||
"DESCRIPTION": "Manage your account security settings.",
|
||||
"LINK_TEXT": "Learn more about SAML SSO",
|
||||
"SAML_DISABLED_MESSAGE": "SAML SSO is currently disabled. Please contact your administrator to enable this feature.",
|
||||
"SAML": {
|
||||
"TITLE": "SAML SSO",
|
||||
"NOTE": "Configure SAML single sign-on for your account. Users will authenticate through your identity provider instead of using email/password.",
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"YEAR_IN_REVIEW": {
|
||||
"TITLE": "Year in Review",
|
||||
"LOADING": "Loading your year in review...",
|
||||
"ERROR": "Failed to load year in review",
|
||||
"CLOSE": "Tanca",
|
||||
"CONVERSATIONS": {
|
||||
"TITLE": "You have handled",
|
||||
"SUBTITLE": "converses",
|
||||
"FALLBACK": "This year wasn't about the numbers. It was about showing up.",
|
||||
"COMPARISON": {
|
||||
"0_50": "You showed up, and that's how every good inbox begins.",
|
||||
"50_100": "You kept the replies flowing and the conversations alive.",
|
||||
"100_500": "You handled serious volume and kept everything on track.",
|
||||
"500_2000": "You kept things moving while the volume kept climbing.",
|
||||
"2000_10000": "You ran high traffic through your inbox without breaking a sweat.",
|
||||
"10000_PLUS": "That's a full city of customers knocking on your door. You made it look effortless."
|
||||
}
|
||||
},
|
||||
"BUSIEST_DAY": {
|
||||
"TITLE": "Your busiest day was",
|
||||
"MESSAGE": "{count} conversations that day.",
|
||||
"COMPARISON": {
|
||||
"0_5": "A warm-up lap that barely woke the inbox.",
|
||||
"5_10": "Enough action to justify a second cup of coffee.",
|
||||
"10_25": "Things got busy and the inbox stayed on its toes.",
|
||||
"25_50": "A proper rush that barely broke a sweat.",
|
||||
"50_100": "Controlled chaos, handled like a normal Tuesday.",
|
||||
"100_500": "Absolute dumpster fire, somehow still shipping replies.",
|
||||
"500_PLUS": "The inbox lost all chill and never slowed down."
|
||||
}
|
||||
},
|
||||
"PERSONALITY": {
|
||||
"TITLE": "Your support personality is",
|
||||
"MESSAGES": {
|
||||
"SWIFT_HELPER": "You replied in {time} on average. Faster than most notifications.",
|
||||
"QUICK_RESPONDER": "You replied in {time} on average. The inbox barely waited.",
|
||||
"STEADY_SUPPORT": "You replied in {time} on average. Calm pace, solid replies.",
|
||||
"THOUGHTFUL_ADVISOR": "You replied in {time} on average. Took the time to get it right."
|
||||
}
|
||||
},
|
||||
"THANK_YOU": {
|
||||
"TITLE": "Congratulations on surviving the inbox of {year}.",
|
||||
"MESSAGE": "Thank you for your incredible dedication to supporting customers throughout this year. Your hard work has made a real difference, and we're grateful to have you on this journey. Here's to making {nextYear} even better together!"
|
||||
},
|
||||
"SHARE_MODAL": {
|
||||
"TITLE": "Share Your Year in Review",
|
||||
"PREPARING": "Preparing your image...",
|
||||
"DOWNLOAD": "Descarrega",
|
||||
"SHARE_TITLE": "My {year} Year in Review",
|
||||
"SHARE_TEXT": "Check out my {year} Year in Review with Chatwoot!",
|
||||
"BRANDING": "Made with Chatwoot"
|
||||
},
|
||||
"BANNER": {
|
||||
"TITLE": "Your {year} Year in Review is here",
|
||||
"BUTTON": "See your impact"
|
||||
},
|
||||
"NAVIGATION": {
|
||||
"PREVIOUS": "Previous",
|
||||
"NEXT": "Next",
|
||||
"SHARE": "Share conversation"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,6 +129,10 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"COMPANIES": {
|
||||
"HEADER": "Companies",
|
||||
"SORT_BY": {
|
||||
"LABEL": "Seřadit podle",
|
||||
"OPTIONS": {
|
||||
"NAME": "Název",
|
||||
"DOMAIN": "Domain",
|
||||
"CREATED_AT": "Vytvořeno"
|
||||
}
|
||||
},
|
||||
"ORDER": {
|
||||
"LABEL": "Order",
|
||||
"OPTIONS": {
|
||||
"ASCENDING": "Ascending",
|
||||
"DESCENDING": "Descending"
|
||||
}
|
||||
},
|
||||
"SEARCH_PLACEHOLDER": "Search companies...",
|
||||
"LOADING": "Loading companies...",
|
||||
"UNNAMED": "Unnamed Company",
|
||||
"CONTACTS_COUNT": "{n} contact | {n} contacts",
|
||||
"EMPTY_STATE": {
|
||||
"TITLE": "No companies found"
|
||||
}
|
||||
},
|
||||
"COMPANIES_LAYOUT": {
|
||||
"PAGINATION_FOOTER": {
|
||||
"SHOWING": "Showing {startItem} – {endItem} of {totalItems} company | Showing {startItem} – {endItem} of {totalItems} companies"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,8 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "Call",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -456,6 +457,9 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"LOADING_CONVERSATIONS": "Načítání konverzací",
|
||||
"CANNOT_REPLY": "Nemůžete odpovědět z důvodu",
|
||||
"24_HOURS_WINDOW": "24 hodinové omezení okna",
|
||||
"48_HOURS_WINDOW": "48 hodinové omezení okna",
|
||||
"API_HOURS_WINDOW": "You can only reply to this conversation within {hours} hours",
|
||||
"NOT_ASSIGNED_TO_YOU": "Tato konverzace vám není přiřazena. Chcete si přiřadit tuto konverzaci?",
|
||||
"ASSIGN_TO_ME": "Přiřadit mi",
|
||||
@@ -196,7 +197,6 @@
|
||||
"INSERT_READ_MORE": "Read more",
|
||||
"DISMISS_REPLY": "Dismiss reply",
|
||||
"REPLYING_TO": "Replying to:",
|
||||
"TIP_FORMAT_ICON": "Zobrazit formátovaný textový editor",
|
||||
"TIP_EMOJI_ICON": "Zobrazit výběr emoji",
|
||||
"TIP_ATTACH_ICON": "Přiložit soubory",
|
||||
"TIP_AUDIORECORDER_ICON": "Nahrát zvuk",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user