Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1dd4124b7 |
@@ -47,13 +47,6 @@
|
||||
- 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
|
||||
- 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
|
||||
@@ -85,4 +78,3 @@ Practical checklist for any change impacting core logic or public APIs
|
||||
- Keep request/response contracts stable across OSS and Enterprise; update both sets of routes/controllers when introducing new APIs.
|
||||
- When renaming/moving shared code, mirror the change in `enterprise/` to prevent drift.
|
||||
- Tests: Add Enterprise-specific specs under `spec/enterprise`, mirroring OSS spec layout where applicable.
|
||||
- When modifying existing OSS features for Enterprise-only behavior, add an Enterprise module (via `prepend_mod_with`/`include_mod_with`) instead of editing OSS files directly—especially for policies, controllers, and services. For Enterprise-exclusive features, place code directly under `enterprise/`.
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
class YearInReviewBuilder
|
||||
attr_reader :account, :user_id, :year
|
||||
|
||||
def initialize(account:, user_id:, year:)
|
||||
@account = account
|
||||
@user_id = user_id
|
||||
@year = year
|
||||
end
|
||||
|
||||
def build
|
||||
{
|
||||
year: year,
|
||||
total_conversations: total_conversations_count,
|
||||
busiest_day: busiest_day_data,
|
||||
support_personality: support_personality_data
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def year_range
|
||||
@year_range ||= begin
|
||||
start_time = Time.zone.local(year, 1, 1).beginning_of_day
|
||||
end_time = Time.zone.local(year, 12, 31).end_of_day
|
||||
start_time..end_time
|
||||
end
|
||||
end
|
||||
|
||||
def total_conversations_count
|
||||
account.conversations
|
||||
.where(assignee_id: user_id, created_at: year_range)
|
||||
.count
|
||||
end
|
||||
|
||||
def busiest_day_data
|
||||
daily_counts = account.conversations
|
||||
.where(assignee_id: user_id, created_at: year_range)
|
||||
.group_by_day(:created_at, range: year_range, time_zone: Time.zone)
|
||||
.count
|
||||
|
||||
return nil if daily_counts.empty?
|
||||
|
||||
busiest_date, count = daily_counts.max_by { |_date, cnt| cnt }
|
||||
|
||||
return nil if count.zero?
|
||||
|
||||
{
|
||||
date: busiest_date.strftime('%b %d'),
|
||||
count: count
|
||||
}
|
||||
end
|
||||
|
||||
def support_personality_data
|
||||
response_time = average_response_time
|
||||
|
||||
return { avg_response_time_seconds: 0 } if response_time.nil?
|
||||
|
||||
{
|
||||
avg_response_time_seconds: response_time.to_i
|
||||
}
|
||||
end
|
||||
|
||||
def average_response_time
|
||||
avg_time = account.reporting_events
|
||||
.where(
|
||||
name: 'first_response',
|
||||
user_id: user_id,
|
||||
created_at: year_range
|
||||
)
|
||||
.average(:value)
|
||||
|
||||
avg_time&.to_f
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
@@ -92,8 +92,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def settings_params
|
||||
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label,
|
||||
conversation_required_attributes: [])
|
||||
params.permit(:auto_resolve_after, :auto_resolve_message, :auto_resolve_ignore_waiting, :audio_transcriptions, :auto_resolve_label)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
class Api::V2::Accounts::YearInReviewsController < Api::V1::Accounts::BaseController
|
||||
def show
|
||||
year = params[:year] || 2025
|
||||
cache_key = "year_in_review_#{Current.account.id}_#{year}"
|
||||
|
||||
cached_data = Current.user.ui_settings&.dig(cache_key)
|
||||
|
||||
if cached_data.present?
|
||||
render json: cached_data
|
||||
else
|
||||
builder = YearInReviewBuilder.new(
|
||||
account: Current.account,
|
||||
user_id: Current.user.id,
|
||||
year: year
|
||||
)
|
||||
|
||||
data = builder.build
|
||||
|
||||
ui_settings = Current.user.ui_settings || {}
|
||||
ui_settings[cache_key] = data
|
||||
Current.user.update(ui_settings: ui_settings)
|
||||
|
||||
render json: data
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -73,7 +73,6 @@ 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,7 +46,6 @@ 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]
|
||||
|
||||
@@ -1,41 +1,11 @@
|
||||
class SuperAdmin::DashboardController < SuperAdmin::ApplicationController
|
||||
include ActionView::Helpers::NumberHelper
|
||||
|
||||
ALLOWED_CHART_DAYS = [7, 30].freeze
|
||||
|
||||
def index
|
||||
@chart_days = parse_chart_days
|
||||
@data = conversations_by_day
|
||||
@accounts_count = format_count(Account.count)
|
||||
@users_count = format_count(User.count)
|
||||
@inboxes_count = format_count(Inbox.count)
|
||||
@conversations_count = format_count(recent_conversations.count)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_chart_days
|
||||
days = params[:chart_period].to_i
|
||||
ALLOWED_CHART_DAYS.include?(days) ? days : 7
|
||||
end
|
||||
|
||||
def chart_date_range
|
||||
(@chart_days - 1).days.ago.beginning_of_day..Time.current
|
||||
end
|
||||
|
||||
def last_30_days
|
||||
29.days.ago.beginning_of_day..Time.current
|
||||
end
|
||||
|
||||
def recent_conversations
|
||||
Conversation.unscoped.where(created_at: last_30_days)
|
||||
end
|
||||
|
||||
def conversations_by_day
|
||||
Conversation.unscoped.group_by_day(:created_at, range: chart_date_range, default_value: 0).count.to_a
|
||||
end
|
||||
|
||||
def format_count(count)
|
||||
number_with_delimiter(count)
|
||||
@data = Conversation.unscoped.group_by_day(:created_at, range: 30.days.ago..2.seconds.ago).count.to_a
|
||||
@accounts_count = number_with_delimiter(Account.count)
|
||||
@users_count = number_with_delimiter(User.count)
|
||||
@inboxes_count = number_with_delimiter(Inbox.count)
|
||||
@conversations_count = number_with_delimiter(Conversation.count)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
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
|
||||
@@ -1,53 +0,0 @@
|
||||
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
|
||||
@@ -31,8 +31,8 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
updated_at: Field::DateTime,
|
||||
users: CountField,
|
||||
conversations: CountField,
|
||||
locale: LocaleField,
|
||||
status: StatusField,
|
||||
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
|
||||
status: Field::Select.with_options(collection: [%w[Active active], %w[Suspended suspended]]),
|
||||
account_users: Field::HasMany,
|
||||
custom_attributes: Field::String
|
||||
}.merge(enterprise_attribute_types).freeze
|
||||
@@ -45,9 +45,10 @@ class AccountDashboard < Administrate::BaseDashboard
|
||||
COLLECTION_ATTRIBUTES = %i[
|
||||
id
|
||||
name
|
||||
status
|
||||
users
|
||||
locale
|
||||
users
|
||||
conversations
|
||||
status
|
||||
].freeze
|
||||
|
||||
# SHOW_PAGE_ATTRIBUTES
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
require 'administrate/field/base'
|
||||
|
||||
class LocaleField < Administrate::Field::Base
|
||||
def to_s
|
||||
language = LANGUAGES_CONFIG.find { |_key, val| val[:iso_639_1_code] == data }
|
||||
language ? language[1][:name] : data
|
||||
end
|
||||
|
||||
def selectable_options
|
||||
LANGUAGES_CONFIG.map { |_key, val| [val[:name], val[:iso_639_1_code]] }
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
require 'administrate/field/base'
|
||||
|
||||
class StatusField < Administrate::Field::Base
|
||||
def to_s
|
||||
data.to_s.capitalize
|
||||
end
|
||||
|
||||
def active?
|
||||
data.to_s == 'active'
|
||||
end
|
||||
|
||||
def selectable_options
|
||||
[%w[Active active], %w[Suspended suspended]]
|
||||
end
|
||||
end
|
||||
@@ -78,12 +78,6 @@ 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.'
|
||||
|
||||
@@ -9,7 +9,7 @@ module SuperAdmin::NavigationHelper
|
||||
end
|
||||
|
||||
# Add general at the beginning
|
||||
general_feature = [['general', { 'config_key' => 'general', 'name' => 'General', 'icon' => 'icon-gear' }]]
|
||||
general_feature = [['general', { 'config_key' => 'general', 'name' => 'General' }]]
|
||||
|
||||
general_feature + features.to_a
|
||||
end
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
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
|
||||
@@ -1,14 +0,0 @@
|
||||
/* 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();
|
||||
@@ -1,35 +0,0 @@
|
||||
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' }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
/* global axios */
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class YearInReviewAPI extends ApiClient {
|
||||
constructor() {
|
||||
super('year_in_review', { accountScoped: true, apiVersion: 'v2' });
|
||||
}
|
||||
|
||||
get(year) {
|
||||
return axios.get(`${this.url}`, {
|
||||
params: { year },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new YearInReviewAPI();
|
||||
@@ -44,7 +44,6 @@ 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',
|
||||
};
|
||||
@@ -66,7 +65,6 @@ const defaultState = {
|
||||
facebook: '',
|
||||
github: '',
|
||||
instagram: '',
|
||||
tiktok: '',
|
||||
linkedin: '',
|
||||
twitter: '',
|
||||
},
|
||||
|
||||
+1
-6
@@ -40,12 +40,7 @@ defineExpose({ dialogRef, contactsFormRef, onSuccess });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog
|
||||
ref="dialogRef"
|
||||
width="3xl"
|
||||
overflow-y-auto
|
||||
@confirm="handleDialogConfirm"
|
||||
>
|
||||
<Dialog ref="dialogRef" width="3xl" @confirm="handleDialogConfirm">
|
||||
<ContactsForm
|
||||
ref="contactsFormRef"
|
||||
is-new-contact
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
<script setup>
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
import AttributeBadge from 'dashboard/components-next/CustomAttributes/AttributeBadge.vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
attribute: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
badges: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['edit', 'delete']);
|
||||
|
||||
const iconByType = {
|
||||
text: 'i-lucide-align-justify',
|
||||
checkbox: 'i-lucide-circle-check-big',
|
||||
list: 'i-lucide-list',
|
||||
date: 'i-lucide-calendar',
|
||||
link: 'i-lucide-link',
|
||||
number: 'i-lucide-hash',
|
||||
};
|
||||
|
||||
const attributeIcon = computed(() => {
|
||||
const typeKey = props.attribute.type?.toLowerCase();
|
||||
return iconByType[typeKey] || 'i-lucide-align-justify';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col gap-2 p-4 bg-n-solid-1 rounded-2xl outline outline-1 outline-n-container"
|
||||
>
|
||||
<div class="flex flex-wrap gap-2 justify-between items-center">
|
||||
<div class="flex flex-wrap gap-2 items-center min-w-0">
|
||||
<h4 class="text-sm font-medium truncate text-n-slate-12">
|
||||
{{ attribute.label }}
|
||||
</h4>
|
||||
<div class="w-px h-3 bg-n-strong" />
|
||||
<div class="flex gap-2 items-center text-sm text-n-slate-11">
|
||||
<div class="flex items-center gap-1.5 text-n-slate-11">
|
||||
<Icon :icon="attributeIcon" class="size-4" />
|
||||
<span class="text-sm">{{ attribute.type }}</span>
|
||||
</div>
|
||||
<div class="w-px h-3 bg-n-weak" />
|
||||
<div class="flex items-center gap-1.5 text-n-slate-11">
|
||||
<Icon icon="i-lucide-key-round" class="size-4" />
|
||||
<span class="line-clamp-1 text-sm">{{ attribute.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<AttributeBadge
|
||||
v-for="badge in badges"
|
||||
:key="badge.type"
|
||||
:type="badge.type"
|
||||
/>
|
||||
<div
|
||||
v-if="badges.length > 0"
|
||||
class="w-px h-3 bg-n-strong ltr:ml-1.5 rtl:mr-1.5"
|
||||
/>
|
||||
<Button
|
||||
icon="i-lucide-pencil-line"
|
||||
size="sm"
|
||||
color="slate"
|
||||
ghost
|
||||
@click="emit('edit', attribute)"
|
||||
/>
|
||||
<div class="w-px h-3 bg-n-strong" />
|
||||
<Button
|
||||
icon="i-lucide-trash"
|
||||
size="sm"
|
||||
color="slate"
|
||||
ghost
|
||||
@click="emit('delete', attribute)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mb-0 text-sm text-n-slate-11">
|
||||
{{ attribute.attribute_description || attribute.description || '' }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,42 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'resolution',
|
||||
validator: value => ['pre-chat', 'resolution'].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const attributeConfig = {
|
||||
'pre-chat': {
|
||||
colorClass: 'text-n-blue-11',
|
||||
icon: 'i-lucide-message-circle',
|
||||
labelKey: 'ATTRIBUTES_MGMT.BADGES.PRE_CHAT',
|
||||
},
|
||||
resolution: {
|
||||
colorClass: 'text-n-teal-11',
|
||||
icon: 'i-lucide-circle-check-big',
|
||||
labelKey: 'ATTRIBUTES_MGMT.BADGES.RESOLUTION',
|
||||
},
|
||||
};
|
||||
const config = computed(
|
||||
() => attributeConfig[props.type] || attributeConfig.resolution
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex gap-1 justify-center items-center px-1.5 py-1 rounded-md shadow outline-1 outline outline-n-container bg-n-solid-2"
|
||||
>
|
||||
<Icon :icon="config.icon" class="size-4" :class="config.colorClass" />
|
||||
<span class="text-xs" :class="config.colorClass">{{
|
||||
t(config.labelKey)
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -9,8 +9,6 @@ import { useAlert } from 'dashboard/composables';
|
||||
import { ExceptionWithMessage } from 'shared/helpers/CustomErrors';
|
||||
import { debounce } from '@chatwoot/utils';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
import {
|
||||
searchContacts,
|
||||
createNewContact,
|
||||
@@ -228,8 +226,6 @@ const keyboardEvents = {
|
||||
action: () => {
|
||||
if (showComposeNewConversation.value) {
|
||||
showComposeNewConversation.value = false;
|
||||
emit('close');
|
||||
emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, false);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useFileUpload } from 'dashboard/composables/useFileUpload';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { useEventListener } from '@vueuse/core';
|
||||
import { ALLOWED_FILE_TYPES } from 'shared/constants/messages';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import FileUpload from 'vue-upload-component';
|
||||
@@ -164,20 +163,6 @@ const keyboardEvents = {
|
||||
},
|
||||
};
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
const onPaste = e => {
|
||||
if (!props.isEmailOrWebWidgetInbox) return;
|
||||
|
||||
const files = e.clipboardData?.files;
|
||||
if (!files?.length) return;
|
||||
|
||||
Array.from(files).forEach(file => {
|
||||
const { name, type, size } = file;
|
||||
onFileUpload({ file, name, type, size });
|
||||
});
|
||||
};
|
||||
|
||||
useEventListener(document, 'paste', onPaste);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
+1
-4
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useBranding } from 'shared/composables/useBranding';
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import DocumentCard from 'dashboard/components-next/captain/assistant/DocumentCard.vue';
|
||||
@@ -10,8 +9,6 @@ import { documentsList } from 'dashboard/components-next/captain/pageComponents/
|
||||
const emit = defineEmits(['click']);
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
|
||||
const { replaceInstallationName } = useBranding();
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
};
|
||||
@@ -38,7 +35,7 @@ const onClick = () => {
|
||||
v-for="(document, index) in documentsList.slice(0, 5)"
|
||||
:id="document.id"
|
||||
:key="`document-${index}`"
|
||||
:name="replaceInstallationName(document.name)"
|
||||
:name="document.name"
|
||||
:assistant="document.assistant"
|
||||
:external-link="document.external_link"
|
||||
:created-at="document.created_at"
|
||||
|
||||
+2
-4
@@ -1,6 +1,5 @@
|
||||
<script setup>
|
||||
import { useAccount } from 'dashboard/composables/useAccount';
|
||||
import { useBranding } from 'shared/composables/useBranding';
|
||||
import EmptyStateLayout from 'dashboard/components-next/EmptyStateLayout.vue';
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import ResponseCard from 'dashboard/components-next/captain/assistant/ResponseCard.vue';
|
||||
@@ -27,7 +26,6 @@ const isApproved = computed(() => props.variant === 'approved');
|
||||
const isPending = computed(() => props.variant === 'pending');
|
||||
|
||||
const { isOnChatwootCloud } = useAccount();
|
||||
const { replaceInstallationName } = useBranding();
|
||||
|
||||
const onClick = () => {
|
||||
emit('click');
|
||||
@@ -65,8 +63,8 @@ const onClearFilters = () => {
|
||||
v-for="(response, index) in responsesList.slice(0, 5)"
|
||||
:id="response.id"
|
||||
:key="`response-${index}`"
|
||||
:question="replaceInstallationName(response.question)"
|
||||
:answer="replaceInstallationName(response.answer)"
|
||||
:question="response.question"
|
||||
:answer="response.answer"
|
||||
:status="response.status"
|
||||
:assistant="response.assistant"
|
||||
:created-at="response.created_at"
|
||||
|
||||
@@ -13,7 +13,6 @@ 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,12 +61,6 @@ 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,7 +28,6 @@ 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';
|
||||
@@ -318,7 +317,6 @@ 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,7 +20,6 @@ const {
|
||||
isAWhatsAppChannel,
|
||||
isAnEmailChannel,
|
||||
isAnInstagramChannel,
|
||||
isATiktokChannel,
|
||||
} = useInbox();
|
||||
|
||||
const {
|
||||
@@ -61,8 +60,7 @@ const isSent = computed(() => {
|
||||
isAFacebookInbox.value ||
|
||||
isASmsInbox.value ||
|
||||
isATelegramChannel.value ||
|
||||
isAnInstagramChannel.value ||
|
||||
isATiktokChannel.value
|
||||
isAnInstagramChannel.value
|
||||
) {
|
||||
return sourceId.value && status.value === MESSAGE_STATUS.SENT;
|
||||
}
|
||||
@@ -80,8 +78,7 @@ const isDelivered = computed(() => {
|
||||
isAWhatsAppChannel.value ||
|
||||
isATwilioChannel.value ||
|
||||
isASmsInbox.value ||
|
||||
isAFacebookInbox.value ||
|
||||
isATiktokChannel.value
|
||||
isAFacebookInbox.value
|
||||
) {
|
||||
return sourceId.value && status.value === MESSAGE_STATUS.DELIVERED;
|
||||
}
|
||||
@@ -103,8 +100,7 @@ const isRead = computed(() => {
|
||||
isAWhatsAppChannel.value ||
|
||||
isATwilioChannel.value ||
|
||||
isAFacebookInbox.value ||
|
||||
isAnInstagramChannel.value ||
|
||||
isATiktokChannel.value
|
||||
isAnInstagramChannel.value
|
||||
) {
|
||||
return sourceId.value && status.value === MESSAGE_STATUS.READ;
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
<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,7 +49,6 @@ export const ATTACHMENT_TYPES = {
|
||||
STORY_MENTION: 'story_mention',
|
||||
CONTACT: 'contact',
|
||||
IG_REEL: 'ig_reel',
|
||||
EMBED: 'embed',
|
||||
IG_POST: 'ig_post',
|
||||
IG_STORY: 'ig_story',
|
||||
};
|
||||
|
||||
@@ -9,14 +9,11 @@ import { useI18n } from 'vue-i18n';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useSidebarKeyboardShortcuts } from './useSidebarKeyboardShortcuts';
|
||||
import { vOnClickOutside } from '@vueuse/components';
|
||||
import { emitter } from 'shared/helpers/mitt';
|
||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||
|
||||
import Button from 'dashboard/components-next/button/Button.vue';
|
||||
import SidebarGroup from './SidebarGroup.vue';
|
||||
import SidebarProfileMenu from './SidebarProfileMenu.vue';
|
||||
import SidebarChangelogCard from './SidebarChangelogCard.vue';
|
||||
import YearInReviewBanner from '../year-in-review/YearInReviewBanner.vue';
|
||||
import ChannelLeaf from './ChannelLeaf.vue';
|
||||
import SidebarAccountSwitcher from './SidebarAccountSwitcher.vue';
|
||||
import Logo from 'next/icon/Logo.vue';
|
||||
@@ -99,15 +96,6 @@ const closeMobileSidebar = () => {
|
||||
emit('closeMobileSidebar');
|
||||
};
|
||||
|
||||
const onComposeOpen = toggleFn => {
|
||||
toggleFn();
|
||||
emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, true);
|
||||
};
|
||||
|
||||
const onComposeClose = () => {
|
||||
emitter.emit(BUS_EVENTS.NEW_CONVERSATION_MODAL, false);
|
||||
};
|
||||
|
||||
const newReportRoutes = () => [
|
||||
{
|
||||
name: 'Reports Agent',
|
||||
@@ -635,14 +623,14 @@ const menuItems = computed(() => {
|
||||
{{ searchShortcut }}
|
||||
</span>
|
||||
</RouterLink>
|
||||
<ComposeConversation align-position="right" @close="onComposeClose">
|
||||
<ComposeConversation align-position="right">
|
||||
<template #trigger="{ toggle }">
|
||||
<Button
|
||||
icon="i-lucide-pen-line"
|
||||
color="slate"
|
||||
size="sm"
|
||||
class="!h-7 !bg-n-solid-3 dark:!bg-n-black/30 !outline-n-weak !text-n-slate-11"
|
||||
@click="onComposeOpen(toggle)"
|
||||
@click="toggle"
|
||||
/>
|
||||
</template>
|
||||
</ComposeConversation>
|
||||
@@ -663,7 +651,6 @@ const menuItems = computed(() => {
|
||||
<div
|
||||
class="pointer-events-none absolute inset-x-0 -top-[31px] h-8 bg-gradient-to-t from-n-solid-2 to-transparent"
|
||||
/>
|
||||
<YearInReviewBanner />
|
||||
<SidebarChangelogCard
|
||||
v-if="isOnChatwootCloud && !isACustomBrandedInstance"
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import Auth from 'dashboard/api/auth';
|
||||
import { useMapGetter } from 'dashboard/composables/store';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import Avatar from 'next/avatar/Avatar.vue';
|
||||
import SidebarProfileMenuStatus from './SidebarProfileMenuStatus.vue';
|
||||
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
|
||||
import YearInReviewModal from 'dashboard/components-next/year-in-review/YearInReviewModal.vue';
|
||||
|
||||
import {
|
||||
DropdownContainer,
|
||||
@@ -24,7 +22,6 @@ defineOptions({
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
const { uiSettings } = useUISettings();
|
||||
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const currentUserAvailability = useMapGetter('getCurrentUserAvailability');
|
||||
@@ -34,29 +31,6 @@ const isFeatureEnabledonAccount = useMapGetter(
|
||||
'accounts/isFeatureEnabledonAccount'
|
||||
);
|
||||
|
||||
const showYearInReviewModal = ref(false);
|
||||
|
||||
const bannerClosedKey = computed(() => {
|
||||
return `yir_closed_${accountId.value}_2025`;
|
||||
});
|
||||
|
||||
const isBannerClosed = computed(() => {
|
||||
return uiSettings.value?.[bannerClosedKey.value] === true;
|
||||
});
|
||||
|
||||
const showYearInReviewMenuItem = computed(() => {
|
||||
return isBannerClosed.value;
|
||||
});
|
||||
|
||||
const openYearInReviewModal = () => {
|
||||
showYearInReviewModal.value = true;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const closeYearInReviewModal = () => {
|
||||
showYearInReviewModal.value = false;
|
||||
};
|
||||
|
||||
const showChatSupport = computed(() => {
|
||||
return (
|
||||
isFeatureEnabledonAccount.value(
|
||||
@@ -68,13 +42,6 @@ const showChatSupport = computed(() => {
|
||||
|
||||
const menuItems = computed(() => {
|
||||
return [
|
||||
{
|
||||
show: showYearInReviewMenuItem.value,
|
||||
showOnCustomBrandedInstance: false,
|
||||
label: t('SIDEBAR_ITEMS.YEAR_IN_REVIEW'),
|
||||
icon: 'i-lucide-gift',
|
||||
click: openYearInReviewModal,
|
||||
},
|
||||
{
|
||||
show: showChatSupport.value,
|
||||
showOnCustomBrandedInstance: false,
|
||||
@@ -190,9 +157,4 @@ const allowedMenuItems = computed(() => {
|
||||
</template>
|
||||
</DropdownBody>
|
||||
</DropdownContainer>
|
||||
|
||||
<YearInReviewModal
|
||||
:show="showYearInReviewModal"
|
||||
@close="closeYearInReviewModal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { toPng } from 'html-to-image';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
slideElement: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
slideBackground: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
year: {
|
||||
type: [Number, String],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const isGenerating = ref(false);
|
||||
const shareImageUrl = ref(null);
|
||||
|
||||
const generateImage = async () => {
|
||||
if (!props.slideElement) return;
|
||||
|
||||
isGenerating.value = true;
|
||||
try {
|
||||
let slideElement = props.slideElement;
|
||||
|
||||
if (slideElement && '$el' in slideElement) {
|
||||
slideElement = slideElement.$el;
|
||||
}
|
||||
|
||||
if (!slideElement) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('No slide element found');
|
||||
return;
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
'bg-[#5BD58A]': '#5BD58A',
|
||||
'bg-[#60a5fa]': '#60a5fa',
|
||||
'bg-[#fb923c]': '#fb923c',
|
||||
'bg-[#f87171]': '#f87171',
|
||||
'bg-[#fbbf24]': '#fbbf24',
|
||||
};
|
||||
const bgColor = colorMap[props.slideBackground] || '#ffffff';
|
||||
|
||||
const dataUrl = await toPng(slideElement, {
|
||||
pixelRatio: 1.2,
|
||||
backgroundColor: bgColor,
|
||||
// Skip font/CSS embedding to avoid CORS issues with CDN stylesheets
|
||||
// See: https://github.com/bubkoo/html-to-image/issues/49#issuecomment-762222100
|
||||
fontEmbedCSS: '',
|
||||
cacheBust: true,
|
||||
});
|
||||
|
||||
const img = new Image();
|
||||
img.src = dataUrl;
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve;
|
||||
img.onerror = reject;
|
||||
});
|
||||
|
||||
const finalCanvas = document.createElement('canvas');
|
||||
const borderSize = 20;
|
||||
const bottomPadding = 50;
|
||||
|
||||
finalCanvas.width = img.width + borderSize * 2;
|
||||
finalCanvas.height = img.height + borderSize * 2 + bottomPadding;
|
||||
|
||||
const ctx = finalCanvas.getContext('2d');
|
||||
|
||||
ctx.fillStyle = 'white';
|
||||
ctx.fillRect(0, 0, finalCanvas.width, finalCanvas.height);
|
||||
|
||||
ctx.drawImage(img, borderSize, borderSize);
|
||||
|
||||
ctx.fillStyle = '#1f2d3d';
|
||||
ctx.font = 'normal 16px system-ui, -apple-system, sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(
|
||||
t('YEAR_IN_REVIEW.SHARE_MODAL.BRANDING'),
|
||||
borderSize,
|
||||
img.height + borderSize + 35
|
||||
);
|
||||
|
||||
const logo = new Image();
|
||||
logo.src = '/brand-assets/logo.svg';
|
||||
await new Promise(resolve => {
|
||||
logo.onload = resolve;
|
||||
});
|
||||
|
||||
const logoHeight = 30;
|
||||
const logoWidth = (logo.width / logo.height) * logoHeight;
|
||||
const logoX = finalCanvas.width - borderSize - logoWidth;
|
||||
const logoY = img.height + borderSize + 15;
|
||||
|
||||
ctx.drawImage(logo, logoX, logoY, logoWidth, logoHeight);
|
||||
|
||||
shareImageUrl.value = finalCanvas.toDataURL('image/png');
|
||||
} catch (err) {
|
||||
// Handle errors silently for now
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to generate image:', err);
|
||||
} finally {
|
||||
isGenerating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const downloadImage = () => {
|
||||
if (!shareImageUrl.value) return;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = shareImageUrl.value;
|
||||
link.download = `chatwoot-year-in-review-${props.year}.png`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
const shareImage = async () => {
|
||||
if (!shareImageUrl.value) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(shareImageUrl.value);
|
||||
const blob = await response.blob();
|
||||
const file = new File([blob], `chatwoot-year-in-review-${props.year}.png`, {
|
||||
type: 'image/png',
|
||||
});
|
||||
|
||||
if (
|
||||
navigator.share &&
|
||||
navigator.canShare &&
|
||||
navigator.canShare({ files: [file] })
|
||||
) {
|
||||
await navigator.share({
|
||||
title: t('YEAR_IN_REVIEW.SHARE_MODAL.SHARE_TITLE', {
|
||||
year: props.year,
|
||||
}),
|
||||
text: t('YEAR_IN_REVIEW.SHARE_MODAL.SHARE_TEXT', { year: props.year }),
|
||||
files: [file],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
downloadImage();
|
||||
} catch (err) {
|
||||
// Fallback to download if sharing fails
|
||||
downloadImage();
|
||||
}
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
shareImageUrl.value = null;
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const handleOpen = async () => {
|
||||
if (props.show && !shareImageUrl.value) {
|
||||
await generateImage();
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({ handleOpen });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-[10001]"
|
||||
@click="close"
|
||||
>
|
||||
<div v-if="isGenerating" class="flex items-center justify-center">
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="inline-block w-12 h-12 border-4 rounded-full border-white border-t-transparent animate-spin"
|
||||
/>
|
||||
<p class="mt-4 text-sm text-white">
|
||||
{{ t('YEAR_IN_REVIEW.SHARE_MODAL.PREPARING') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="shareImageUrl"
|
||||
class="max-w-2xl w-full mx-4 flex flex-col gap-6 bg-slate-800 rounded-2xl p-6"
|
||||
@click.stop
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-xl font-medium text-white">
|
||||
{{ t('YEAR_IN_REVIEW.SHARE_MODAL.TITLE') }}
|
||||
</h3>
|
||||
<button
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full text-white hover:bg-white hover:bg-opacity-20 transition-colors"
|
||||
@click="close"
|
||||
>
|
||||
<i class="i-lucide-x w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<img
|
||||
:src="shareImageUrl"
|
||||
alt="Year in Review"
|
||||
class="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
class="flex-[2] px-4 py-3 flex items-center justify-center gap-2 rounded-full text-white bg-white bg-opacity-20 hover:bg-opacity-30 transition-colors"
|
||||
@click="downloadImage"
|
||||
>
|
||||
<i class="i-lucide-download w-5 h-5" />
|
||||
<span class="text-sm font-medium">{{
|
||||
t('YEAR_IN_REVIEW.SHARE_MODAL.DOWNLOAD')
|
||||
}}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full text-white bg-white bg-opacity-20 hover:bg-opacity-30 transition-colors"
|
||||
@click="shareImage"
|
||||
>
|
||||
<i class="i-lucide-share-2 w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -1,88 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import YearInReviewModal from './YearInReviewModal.vue';
|
||||
import Icon from 'dashboard/components-next/icon/Icon.vue';
|
||||
|
||||
const yearInReviewBannerImage =
|
||||
'/assets/images/dashboard/year-in-review/year-in-review-sidebar.png';
|
||||
const { t } = useI18n();
|
||||
const { uiSettings, updateUISettings } = useUISettings();
|
||||
const getters = useStoreGetters();
|
||||
const showModal = ref(false);
|
||||
const modalRef = ref(null);
|
||||
|
||||
const currentYear = 2025;
|
||||
|
||||
const isACustomBrandedInstance =
|
||||
getters['globalConfig/isACustomBrandedInstance'];
|
||||
|
||||
const bannerClosedKey = computed(() => {
|
||||
const accountId = getters.getCurrentAccountId.value;
|
||||
return `yir_closed_${accountId}_${currentYear}`;
|
||||
});
|
||||
|
||||
const isBannerClosed = computed(() => {
|
||||
return uiSettings.value?.[bannerClosedKey.value] === true;
|
||||
});
|
||||
|
||||
const shouldShowBanner = computed(
|
||||
() => !isBannerClosed.value && !isACustomBrandedInstance.value
|
||||
);
|
||||
|
||||
const openModal = () => {
|
||||
showModal.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
showModal.value = false;
|
||||
};
|
||||
|
||||
const closeBanner = event => {
|
||||
event.stopPropagation();
|
||||
updateUISettings({ [bannerClosedKey.value]: true });
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="shouldShowBanner" class="relative">
|
||||
<div
|
||||
class="mx-2 my-1 p-3 bg-n-iris-9 rounded-lg cursor-pointer hover:shadow-md transition-all"
|
||||
@click="openModal"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2 mb-3">
|
||||
<span
|
||||
class="text-sm font-semibold text-white leading-tight tracking-tight flex-1"
|
||||
>
|
||||
{{ t('YEAR_IN_REVIEW.BANNER.TITLE', { year: currentYear }) }}
|
||||
</span>
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded hover:bg-white hover:bg-opacity-20 transition-colors p-0"
|
||||
@click="closeBanner"
|
||||
>
|
||||
<Icon
|
||||
icon="i-lucide-x size-4 mt-0.5 text-n-slate-1 dark:text-n-slate-12"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<img
|
||||
:src="yearInReviewBannerImage"
|
||||
alt="Year in Review"
|
||||
class="w-full h-auto rounded"
|
||||
/>
|
||||
<button
|
||||
class="w-full px-3 py-2 bg-white text-n-iris-9 text-xs font-medium rounded-mdtracking-tight"
|
||||
@click.stop="openModal"
|
||||
>
|
||||
{{ t('YEAR_IN_REVIEW.BANNER.BUTTON') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<YearInReviewModal ref="modalRef" :show="showModal" @close="closeModal" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,389 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch, nextTick } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import YearInReviewAPI from 'dashboard/api/yearInReview';
|
||||
import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents';
|
||||
import { useUISettings } from 'dashboard/composables/useUISettings';
|
||||
import { useStoreGetters } from 'dashboard/composables/store';
|
||||
import { useTrack } from 'dashboard/composables';
|
||||
import { YEAR_IN_REVIEW_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
|
||||
import IntroSlide from './slides/IntroSlide.vue';
|
||||
import ConversationsSlide from './slides/ConversationsSlide.vue';
|
||||
import BusiestDaySlide from './slides/BusiestDaySlide.vue';
|
||||
import PersonalitySlide from './slides/PersonalitySlide.vue';
|
||||
import ThankYouSlide from './slides/ThankYouSlide.vue';
|
||||
import ShareModal from './ShareModal.vue';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
|
||||
const { t } = useI18n();
|
||||
const { uiSettings } = useUISettings();
|
||||
const getters = useStoreGetters();
|
||||
const isOpen = ref(false);
|
||||
const currentSlide = ref(0);
|
||||
const yearData = ref(null);
|
||||
const isLoading = ref(false);
|
||||
const error = ref(null);
|
||||
const slideRefs = ref([]);
|
||||
const showShareModal = ref(false);
|
||||
const shareModalRef = ref(null);
|
||||
const drumrollAudio = ref(null);
|
||||
|
||||
const hasConversations = computed(() => {
|
||||
return yearData.value?.total_conversations > 0;
|
||||
});
|
||||
|
||||
const totalSlides = computed(() => {
|
||||
if (!hasConversations.value) {
|
||||
return 3;
|
||||
}
|
||||
return 5;
|
||||
});
|
||||
|
||||
const slideIndexMap = computed(() => {
|
||||
if (!hasConversations.value) {
|
||||
return [0, 1, 4];
|
||||
}
|
||||
return [0, 1, 2, 3, 4];
|
||||
});
|
||||
|
||||
const currentVisualSlide = computed(() => {
|
||||
return slideIndexMap.value.indexOf(currentSlide.value);
|
||||
});
|
||||
|
||||
const slideBackgrounds = [
|
||||
'bg-[#5BD58A]',
|
||||
'bg-[#60a5fa]',
|
||||
'bg-[#fb923c]',
|
||||
'bg-[#f87171]',
|
||||
'bg-[#fbbf24]',
|
||||
];
|
||||
|
||||
const playDrumroll = () => {
|
||||
try {
|
||||
if (!drumrollAudio.value) {
|
||||
drumrollAudio.value = new Audio('/audio/dashboard/drumroll.mp3');
|
||||
drumrollAudio.value.volume = 0.5;
|
||||
}
|
||||
|
||||
drumrollAudio.value.currentTime = 0;
|
||||
drumrollAudio.value.play().catch(err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Could not play drumroll:', err);
|
||||
});
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('Error playing drumroll:', err);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchYearInReviewData = async () => {
|
||||
const year = 2025;
|
||||
const accountId = getters.getCurrentAccountId.value;
|
||||
const cacheKey = `year_in_review_${accountId}_${year}`;
|
||||
|
||||
const cachedData = uiSettings.value?.[cacheKey];
|
||||
|
||||
if (cachedData) {
|
||||
yearData.value = cachedData;
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await YearInReviewAPI.get(year);
|
||||
yearData.value = response.data;
|
||||
} catch (err) {
|
||||
error.value = err.message;
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const nextSlide = () => {
|
||||
if (currentSlide.value < 4) {
|
||||
useTrack(YEAR_IN_REVIEW_EVENTS.NEXT_CLICKED);
|
||||
if (!hasConversations.value && currentSlide.value === 1) {
|
||||
currentSlide.value = 4;
|
||||
} else {
|
||||
currentSlide.value += 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const previousSlide = () => {
|
||||
if (currentSlide.value > 0) {
|
||||
if (!hasConversations.value && currentSlide.value === 4) {
|
||||
currentSlide.value = 1;
|
||||
} else {
|
||||
currentSlide.value -= 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const goToSlide = visualIndex => {
|
||||
currentSlide.value = slideIndexMap.value[visualIndex];
|
||||
};
|
||||
|
||||
const close = () => {
|
||||
currentSlide.value = 0;
|
||||
isOpen.value = false;
|
||||
yearData.value = null;
|
||||
isLoading.value = false;
|
||||
error.value = null;
|
||||
|
||||
emit('close');
|
||||
};
|
||||
|
||||
const open = () => {
|
||||
useTrack(YEAR_IN_REVIEW_EVENTS.MODAL_OPENED);
|
||||
isOpen.value = true;
|
||||
fetchYearInReviewData();
|
||||
playDrumroll();
|
||||
};
|
||||
|
||||
const currentSlideBackground = computed(
|
||||
() => slideBackgrounds[currentSlide.value]
|
||||
);
|
||||
|
||||
const shareCurrentSlide = async () => {
|
||||
useTrack(YEAR_IN_REVIEW_EVENTS.SHARE_CLICKED);
|
||||
showShareModal.value = true;
|
||||
nextTick(() => {
|
||||
if (shareModalRef.value) {
|
||||
shareModalRef.value.handleOpen();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const closeShareModal = () => {
|
||||
showShareModal.value = false;
|
||||
};
|
||||
|
||||
const keyboardEvents = {
|
||||
Escape: { action: close },
|
||||
ArrowLeft: { action: previousSlide },
|
||||
ArrowRight: { action: nextSlide },
|
||||
};
|
||||
|
||||
useKeyboardEvents(keyboardEvents);
|
||||
|
||||
defineExpose({ open, close });
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
newValue => {
|
||||
if (newValue) {
|
||||
open();
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="fixed inset-0 z-[9999] bg-black font-interDisplay"
|
||||
>
|
||||
<div class="relative w-full h-full overflow-hidden">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="flex items-center justify-center w-full h-full bg-n-slate-2"
|
||||
>
|
||||
<div class="text-center">
|
||||
<div
|
||||
class="inline-block w-12 h-12 border-4 rounded-full border-n-slate-6 border-t-n-slate-11 animate-spin"
|
||||
/>
|
||||
<p class="mt-4 text-sm text-n-slate-11">
|
||||
{{ t('YEAR_IN_REVIEW.LOADING') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="error"
|
||||
class="flex items-center justify-center w-full h-full bg-n-slate-2"
|
||||
>
|
||||
<div class="text-center">
|
||||
<p class="text-lg font-semibold text-red-600">
|
||||
{{ t('YEAR_IN_REVIEW.ERROR') }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-n-slate-11">{{ error }}</p>
|
||||
<button
|
||||
class="mt-4 px-4 py-2 rounded-full text-n-slate-12 dark:text-n-slate-1 bg-white bg-opacity-20 hover:bg-opacity-30 transition-colors"
|
||||
@click="close"
|
||||
>
|
||||
<span class="text-sm font-medium">{{
|
||||
t('YEAR_IN_REVIEW.CLOSE')
|
||||
}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="yearData"
|
||||
class="relative w-full h-full"
|
||||
:class="currentSlideBackground"
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
leave-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 translate-x-[30px]"
|
||||
leave-to-class="opacity-0 -translate-x-[30px]"
|
||||
>
|
||||
<IntroSlide
|
||||
v-if="currentSlide === 0"
|
||||
:key="0"
|
||||
:ref="el => (slideRefs[0] = el)"
|
||||
:year="yearData.year"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
leave-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 translate-x-[30px]"
|
||||
leave-to-class="opacity-0 -translate-x-[30px]"
|
||||
>
|
||||
<ConversationsSlide
|
||||
v-if="currentSlide === 1"
|
||||
:key="1"
|
||||
:ref="el => (slideRefs[1] = el)"
|
||||
:total-conversations="yearData.total_conversations"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
leave-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 translate-x-[30px]"
|
||||
leave-to-class="opacity-0 -translate-x-[30px]"
|
||||
>
|
||||
<BusiestDaySlide
|
||||
v-if="
|
||||
currentSlide === 2 && hasConversations && yearData.busiest_day
|
||||
"
|
||||
:key="2"
|
||||
:ref="el => (slideRefs[2] = el)"
|
||||
:busiest-day="yearData.busiest_day"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
leave-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 translate-x-[30px]"
|
||||
leave-to-class="opacity-0 -translate-x-[30px]"
|
||||
>
|
||||
<PersonalitySlide
|
||||
v-if="
|
||||
currentSlide === 3 &&
|
||||
hasConversations &&
|
||||
yearData.support_personality
|
||||
"
|
||||
:key="3"
|
||||
:ref="el => (slideRefs[3] = el)"
|
||||
:support-personality="yearData.support_personality"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
leave-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="opacity-0 translate-x-[30px]"
|
||||
leave-to-class="opacity-0 -translate-x-[30px]"
|
||||
>
|
||||
<ThankYouSlide
|
||||
v-if="currentSlide === 4"
|
||||
:key="4"
|
||||
:ref="el => (slideRefs[4] = el)"
|
||||
:year="yearData.year"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<div
|
||||
class="absolute bottom-8 left-0 right-0 flex items-center justify-between px-8"
|
||||
>
|
||||
<button
|
||||
v-if="currentSlide > 0"
|
||||
class="px-4 py-2 flex items-center gap-2 rounded-full text-n-slate-12 dark:text-n-slate-1 bg-white bg-opacity-20 hover:bg-opacity-30 transition-colors"
|
||||
@click="previousSlide"
|
||||
>
|
||||
<i class="i-lucide-chevron-left w-5 h-5" />
|
||||
<span class="text-sm font-medium">
|
||||
{{ t('YEAR_IN_REVIEW.NAVIGATION.PREVIOUS') }}
|
||||
</span>
|
||||
</button>
|
||||
<div v-else class="w-20" />
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
v-for="index in totalSlides"
|
||||
:key="index"
|
||||
class="w-2 h-2 rounded-full transition-all"
|
||||
:class="
|
||||
currentVisualSlide === index - 1
|
||||
? 'bg-white w-8'
|
||||
: 'bg-white bg-opacity-50'
|
||||
"
|
||||
@click="goToSlide(index - 1)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-4 py-2 flex items-center gap-2 rounded-full text-n-slate-12 dark:text-n-slate-1 bg-white bg-opacity-20 hover:bg-opacity-30 transition-colors"
|
||||
:class="{ invisible: currentVisualSlide === totalSlides - 1 }"
|
||||
@click="nextSlide"
|
||||
>
|
||||
<span
|
||||
v-if="currentVisualSlide < totalSlides - 1"
|
||||
class="text-sm font-medium"
|
||||
>
|
||||
{{ t('YEAR_IN_REVIEW.NAVIGATION.NEXT') }}
|
||||
</span>
|
||||
<i
|
||||
v-if="currentVisualSlide < totalSlides - 1"
|
||||
class="i-lucide-chevron-right w-5 h-5"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="absolute top-4 left-4 px-4 py-2 flex items-center gap-2 rounded-full text-n-slate-12 dark:text-n-slate-1 bg-white bg-opacity-20 hover:bg-opacity-30 transition-colors"
|
||||
@click="shareCurrentSlide"
|
||||
>
|
||||
<i class="i-lucide-share-2 w-5 h-5" />
|
||||
<span class="text-sm font-medium">{{
|
||||
t('YEAR_IN_REVIEW.NAVIGATION.SHARE')
|
||||
}}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="absolute top-4 right-4 w-10 h-10 flex items-center justify-center rounded-full text-n-slate-12 dark:text-n-slate-1 hover:bg-white hover:bg-opacity-20 transition-colors"
|
||||
@click="close"
|
||||
>
|
||||
<i class="i-lucide-x w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ShareModal
|
||||
ref="shareModalRef"
|
||||
:show="showShareModal"
|
||||
:slide-element="slideRefs[currentSlide]"
|
||||
:slide-background="currentSlideBackground"
|
||||
:year="yearData?.year"
|
||||
@close="closeShareModal"
|
||||
/>
|
||||
</Teleport>
|
||||
</template>
|
||||
@@ -1,76 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
busiestDay: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const coffeeImage =
|
||||
'/assets/images/dashboard/year-in-review/third-frame-coffee.png';
|
||||
const doubleQuotesImage =
|
||||
'/assets/images/dashboard/year-in-review/double-quotes.png';
|
||||
|
||||
const performanceHelperText = computed(() => {
|
||||
const count = props.busiestDay.count;
|
||||
if (count <= 5) return t('YEAR_IN_REVIEW.BUSIEST_DAY.COMPARISON.0_5');
|
||||
if (count <= 10) return t('YEAR_IN_REVIEW.BUSIEST_DAY.COMPARISON.5_10');
|
||||
if (count <= 25) return t('YEAR_IN_REVIEW.BUSIEST_DAY.COMPARISON.10_25');
|
||||
if (count <= 50) return t('YEAR_IN_REVIEW.BUSIEST_DAY.COMPARISON.25_50');
|
||||
if (count <= 100) return t('YEAR_IN_REVIEW.BUSIEST_DAY.COMPARISON.50_100');
|
||||
if (count <= 500) return t('YEAR_IN_REVIEW.BUSIEST_DAY.COMPARISON.100_500');
|
||||
return t('YEAR_IN_REVIEW.BUSIEST_DAY.COMPARISON.500_PLUS');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0 flex items-center justify-center px-8 md:px-32">
|
||||
<div class="flex flex-col gap-4 w-full max-w-3xl">
|
||||
<div class="flex items-center justify-center flex-1">
|
||||
<div class="flex items-center justify-between gap-6 flex-1 md:gap-16">
|
||||
<div class="text-white flex gap-2 flex-col">
|
||||
<div class="text-2xl lg:text-3xl xl:text-4xl tracking-tight">
|
||||
{{ t('YEAR_IN_REVIEW.BUSIEST_DAY.TITLE') }}
|
||||
</div>
|
||||
<div class="text-6xl md:text-8xl lg:text-[140px] tracking-tighter">
|
||||
{{ busiestDay.date }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
:src="coffeeImage"
|
||||
alt="Coffee"
|
||||
class="w-auto h-32 md:h-56 lg:h-72"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<div class="flex items-center justify-center gap-3 md:gap-8">
|
||||
<img
|
||||
:src="doubleQuotesImage"
|
||||
alt="Quote"
|
||||
class="w-8 h-8 md:w-12 md:h-12 lg:w-16 lg:h-16"
|
||||
/>
|
||||
<div class="flex-1">
|
||||
<p
|
||||
class="text-xl md:text-3xl lg:text-4xl font-medium tracking-[-0.2px] text-n-slate-12 dark:text-n-slate-1"
|
||||
>
|
||||
{{
|
||||
t('YEAR_IN_REVIEW.BUSIEST_DAY.MESSAGE', {
|
||||
count: busiestDay.count,
|
||||
})
|
||||
}}
|
||||
{{ performanceHelperText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,94 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
totalConversations: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const cloudImage =
|
||||
'/assets/images/dashboard/year-in-review/second-frame-cloud-icon.png';
|
||||
const doubleQuotesImage =
|
||||
'/assets/images/dashboard/year-in-review/double-quotes.png';
|
||||
|
||||
const hasData = computed(() => {
|
||||
return props.totalConversations > 0;
|
||||
});
|
||||
|
||||
const formatNumber = num => {
|
||||
if (num >= 100000) {
|
||||
return '100k+';
|
||||
}
|
||||
return new Intl.NumberFormat().format(num);
|
||||
};
|
||||
|
||||
const performanceHelperText = computed(() => {
|
||||
if (!hasData.value) {
|
||||
return t('YEAR_IN_REVIEW.CONVERSATIONS.FALLBACK');
|
||||
}
|
||||
|
||||
const count = props.totalConversations;
|
||||
if (count <= 50) return t('YEAR_IN_REVIEW.CONVERSATIONS.COMPARISON.0_50');
|
||||
if (count <= 100) return t('YEAR_IN_REVIEW.CONVERSATIONS.COMPARISON.50_100');
|
||||
if (count <= 500) return t('YEAR_IN_REVIEW.CONVERSATIONS.COMPARISON.100_500');
|
||||
if (count <= 2000)
|
||||
return t('YEAR_IN_REVIEW.CONVERSATIONS.COMPARISON.500_2000');
|
||||
if (count <= 10000)
|
||||
return t('YEAR_IN_REVIEW.CONVERSATIONS.COMPARISON.2000_10000');
|
||||
return t('YEAR_IN_REVIEW.CONVERSATIONS.COMPARISON.10000_PLUS');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center px-8 md:px-32 py-20"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col gap-16"
|
||||
:class="totalConversations > 100 ? 'max-w-4xl' : 'max-w-3xl'"
|
||||
>
|
||||
<div class="flex items-center justify-center flex-1">
|
||||
<div
|
||||
class="flex items-center justify-between gap-6 flex-1"
|
||||
:class="totalConversations > 100 ? 'md:gap-16' : 'md:gap-8'"
|
||||
>
|
||||
<div class="text-white flex gap-3 flex-col">
|
||||
<div class="text-2xl md:text-3xl lg:text-4xl tracking-tight">
|
||||
{{ t('YEAR_IN_REVIEW.CONVERSATIONS.TITLE') }}
|
||||
</div>
|
||||
<div class="text-6xl md:text-8xl lg:text-[180px] tracking-tighter">
|
||||
{{ formatNumber(totalConversations) }}
|
||||
</div>
|
||||
<div class="text-2xl md:text-3xl lg:text-4xl tracking-tight -mt-2">
|
||||
{{ t('YEAR_IN_REVIEW.CONVERSATIONS.SUBTITLE') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img
|
||||
:src="cloudImage"
|
||||
alt="Cloud"
|
||||
class="w-auto h-32 md:h-56 lg:h-80 -mr-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-3 md:gap-6">
|
||||
<img
|
||||
:src="doubleQuotesImage"
|
||||
alt="Quote"
|
||||
class="w-8 h-8 md:w-12 md:h-12 lg:w-16 lg:h-16"
|
||||
/>
|
||||
<p
|
||||
class="text-xl md:text-3xl lg:text-4xl font-medium tracking-[-0.2px] text-n-slate-12 dark:text-n-slate-1"
|
||||
>
|
||||
{{ performanceHelperText }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,43 +0,0 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
year: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const candlesImagePath =
|
||||
'/assets/images/dashboard/year-in-review/first-frame-candles.png';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute inset-0 flex flex-col items-center justify-center text-black px-8 md:px-16 lg:px-24 py-10 md:py-16 lg:py-20 bg-cover bg-center min-h-[700px]"
|
||||
:style="{
|
||||
backgroundImage: `url('/assets/images/dashboard/year-in-review/first-frame-bg.png')`,
|
||||
}"
|
||||
>
|
||||
<div class="text-center max-w-3xl">
|
||||
<h1
|
||||
class="text-8xl md:text-9xl lg:text-[220px] font-semibold mb-4 md:mb-6 leading-none tracking-tight text-n-slate-12 dark:text-n-slate-1"
|
||||
>
|
||||
{{ year }}
|
||||
</h1>
|
||||
<h2
|
||||
class="text-3xl md:text-4xl lg:text-5xl font-medium mb-12 md:mb-16 lg:mb-20 text-n-slate-12 dark:text-n-slate-1"
|
||||
>
|
||||
{{ t('YEAR_IN_REVIEW.TITLE') }}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<img
|
||||
:src="candlesImagePath"
|
||||
alt="Candles"
|
||||
class="absolute bottom-0 left-1/2 transform -translate-x-1/2 w-auto h-32 md:h-48 lg:h-64"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,99 +0,0 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const props = defineProps({
|
||||
supportPersonality: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const clockImage =
|
||||
'/assets/images/dashboard/year-in-review/fourth-frame-clock.png';
|
||||
const doubleQuotesImage =
|
||||
'/assets/images/dashboard/year-in-review/double-quotes.png';
|
||||
|
||||
const formatResponseTime = seconds => {
|
||||
if (seconds < 60) {
|
||||
return 'less than a minute';
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
return minutes === 1 ? '1 minute' : `${minutes} minutes`;
|
||||
}
|
||||
if (seconds < 86400) {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
return hours === 1 ? '1 hour' : `${hours} hours`;
|
||||
}
|
||||
return 'more than a day';
|
||||
};
|
||||
|
||||
const personality = computed(() => {
|
||||
const seconds = props.supportPersonality.avg_response_time_seconds;
|
||||
const minutes = seconds / 60;
|
||||
|
||||
if (minutes < 2) {
|
||||
return 'Swift Helper';
|
||||
}
|
||||
if (minutes < 5) {
|
||||
return 'Quick Responder';
|
||||
}
|
||||
if (minutes < 15) {
|
||||
return 'Steady Support';
|
||||
}
|
||||
return 'Thoughtful Advisor';
|
||||
});
|
||||
|
||||
const personalityMessage = computed(() => {
|
||||
const seconds = props.supportPersonality.avg_response_time_seconds;
|
||||
const time = formatResponseTime(seconds);
|
||||
|
||||
const personalityKeyMap = {
|
||||
'Swift Helper': 'SWIFT_HELPER',
|
||||
'Quick Responder': 'QUICK_RESPONDER',
|
||||
'Steady Support': 'STEADY_SUPPORT',
|
||||
'Thoughtful Advisor': 'THOUGHTFUL_ADVISOR',
|
||||
};
|
||||
|
||||
const key = personalityKeyMap[personality.value];
|
||||
if (!key) return '';
|
||||
|
||||
return t(`YEAR_IN_REVIEW.PERSONALITY.MESSAGES.${key}`, { time });
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0 flex items-center justify-center px-8 md:px-32">
|
||||
<div class="flex flex-col gap-9 max-w-3xl">
|
||||
<div class="mb-4 md:mb-6">
|
||||
<img :src="clockImage" alt="Clock" class="w-auto h-28" />
|
||||
<div class="flex items-center justify-start flex-1 mt-9">
|
||||
<div class="text-n-slate-1 dark:text-n-slate-12 flex gap-3 flex-col">
|
||||
<div class="text-2xl md:text-4xl tracking-tight">
|
||||
{{ t('YEAR_IN_REVIEW.PERSONALITY.TITLE') }}
|
||||
</div>
|
||||
<div class="text-6xl md:text-7xl lg:text-8xl tracking-tighter">
|
||||
{{ personality }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center gap-3 md:gap-6">
|
||||
<img
|
||||
:src="doubleQuotesImage"
|
||||
alt="Quote"
|
||||
class="w-8 h-8 md:w-12 md:h-12 lg:w-16 lg:h-16"
|
||||
/>
|
||||
<p
|
||||
class="text-xl md:text-3xl lg:text-3xl font-medium tracking-[-0.2px] text-n-slate-12 dark:text-n-slate-1"
|
||||
>
|
||||
{{ personalityMessage }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,43 +0,0 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
defineProps({
|
||||
year: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const signatureImage =
|
||||
'/assets/images/dashboard/year-in-review/fifth-frame-signature.png';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="absolute inset-0 flex items-center justify-center px-8 md:px-32 py-20"
|
||||
>
|
||||
<div class="flex flex-col items-start max-w-4xl">
|
||||
<div
|
||||
class="text-3xl md:text-5xl lg:text-6xl font-bold tracking-tight !leading-tight text-n-slate-12 dark:text-n-slate-1"
|
||||
>
|
||||
{{ t('YEAR_IN_REVIEW.THANK_YOU.TITLE', { year }) }}
|
||||
</div>
|
||||
<div
|
||||
class="text-xl lg:text-3xl mt-8 font-medium !leading-snug text-n-slate-12 dark:text-n-slate-1"
|
||||
>
|
||||
{{
|
||||
t('YEAR_IN_REVIEW.THANK_YOU.MESSAGE', { nextYear: Number(year) + 1 })
|
||||
}}
|
||||
</div>
|
||||
<div class="mt-12">
|
||||
<img
|
||||
:src="signatureImage"
|
||||
alt="Chatwoot Team Signature"
|
||||
class="w-auto h-8 md:h-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -23,10 +23,6 @@ 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) {
|
||||
@@ -48,10 +44,6 @@ const isActive = computed(() => {
|
||||
);
|
||||
}
|
||||
|
||||
if (key === 'tiktok') {
|
||||
return props.enabledFeatures.channel_tiktok && hasTiktokConfigured.value;
|
||||
}
|
||||
|
||||
if (key === 'voice') {
|
||||
return props.enabledFeatures.channel_voice;
|
||||
}
|
||||
@@ -65,7 +57,6 @@ const isActive = computed(() => {
|
||||
'telegram',
|
||||
'line',
|
||||
'instagram',
|
||||
'tiktok',
|
||||
'voice',
|
||||
].includes(key);
|
||||
});
|
||||
|
||||
@@ -205,9 +205,6 @@ 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;
|
||||
}
|
||||
@@ -221,9 +218,6 @@ 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,9 +234,6 @@ 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;
|
||||
}
|
||||
@@ -691,10 +688,6 @@ export default {
|
||||
);
|
||||
},
|
||||
onPaste(e) {
|
||||
// Don't handle paste if compose new conversation modal is open
|
||||
if (this.newConversationModalActive) {
|
||||
return;
|
||||
}
|
||||
const data = e.clipboardData.files;
|
||||
if (!this.showRichContentEditor && data.length !== 0) {
|
||||
this.$refs.messageInput?.$el?.blur();
|
||||
|
||||
@@ -48,7 +48,6 @@ 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;
|
||||
},
|
||||
@@ -216,12 +215,6 @@ 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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -273,7 +266,6 @@ describe('useInbox', () => {
|
||||
'is360DialogWhatsAppChannel',
|
||||
'isAnEmailChannel',
|
||||
'isAnInstagramChannel',
|
||||
'isATiktokChannel',
|
||||
'isAVoiceChannel',
|
||||
];
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ 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]: [
|
||||
@@ -25,7 +24,6 @@ export const INBOX_FEATURE_MAP = {
|
||||
INBOX_TYPES.TWITTER,
|
||||
INBOX_TYPES.WHATSAPP,
|
||||
INBOX_TYPES.TELEGRAM,
|
||||
INBOX_TYPES.TIKTOK,
|
||||
INBOX_TYPES.API,
|
||||
],
|
||||
};
|
||||
@@ -130,10 +128,6 @@ 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;
|
||||
});
|
||||
@@ -155,7 +149,6 @@ export const useInbox = (inboxId = null) => {
|
||||
is360DialogWhatsAppChannel,
|
||||
isAnEmailChannel,
|
||||
isAnInstagramChannel,
|
||||
isATiktokChannel,
|
||||
isAVoiceChannel,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -109,11 +109,6 @@ export const FORMATTING = {
|
||||
nodes: [],
|
||||
menu: [],
|
||||
},
|
||||
'Channel::Tiktok': {
|
||||
marks: [],
|
||||
nodes: [],
|
||||
menu: [],
|
||||
},
|
||||
// Special contexts (not actual channels)
|
||||
'Context::Default': {
|
||||
marks: ['strong', 'em', 'code', 'link', 'strike'],
|
||||
|
||||
@@ -37,7 +37,6 @@ 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',
|
||||
|
||||
@@ -131,9 +131,3 @@ export const LINEAR_EVENTS = Object.freeze({
|
||||
LINK_ISSUE: 'Linked a linear issue',
|
||||
UNLINK_ISSUE: 'Unlinked a linear issue',
|
||||
});
|
||||
|
||||
export const YEAR_IN_REVIEW_EVENTS = Object.freeze({
|
||||
MODAL_OPENED: 'Year in Review: Modal opened',
|
||||
NEXT_CLICKED: 'Year in Review: Next clicked',
|
||||
SHARE_CLICKED: 'Year in Review: Share clicked',
|
||||
});
|
||||
|
||||
@@ -10,7 +10,6 @@ export const INBOX_TYPES = {
|
||||
LINE: 'Channel::Line',
|
||||
SMS: 'Channel::Sms',
|
||||
INSTAGRAM: 'Channel::Instagram',
|
||||
TIKTOK: 'Channel::Tiktok',
|
||||
VOICE: 'Channel::Voice',
|
||||
};
|
||||
|
||||
@@ -29,7 +28,6 @@ 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',
|
||||
};
|
||||
|
||||
@@ -45,7 +43,6 @@ 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',
|
||||
};
|
||||
|
||||
@@ -139,9 +136,6 @@ 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';
|
||||
|
||||
|
||||
@@ -38,9 +38,6 @@ 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', () => {
|
||||
@@ -83,10 +80,6 @@ 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');
|
||||
});
|
||||
@@ -109,12 +102,6 @@ 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'
|
||||
|
||||
@@ -129,10 +129,6 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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,8 +18,7 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "ደውል",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"CALL_UNDER_DEVELOPMENT": "መደወል በልማት ላይ ነው",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "የድምፅ ኢንቦክስ ይምረጡ"
|
||||
},
|
||||
@@ -457,9 +456,6 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"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",
|
||||
@@ -197,6 +196,7 @@
|
||||
"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,13 +57,6 @@
|
||||
"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",
|
||||
@@ -388,11 +381,7 @@
|
||||
"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_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"
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "LINE Channel",
|
||||
@@ -478,10 +467,6 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -722,7 +707,6 @@
|
||||
"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.",
|
||||
@@ -1020,7 +1004,6 @@
|
||||
"LINE": "Line",
|
||||
"API": "API Channel",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,6 @@
|
||||
"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",
|
||||
@@ -307,8 +306,6 @@
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Contacts",
|
||||
"ACTIVE": "Active",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -401,44 +398,19 @@
|
||||
"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.",
|
||||
"REFRESH_CREDITS": "Refresh"
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
},
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
},
|
||||
"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.",
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"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,10 +129,6 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "تمكين التحقق من صحة regex"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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,8 +18,7 @@
|
||||
"CREATED_AT_LABEL": "تم إنشاؤها",
|
||||
"NEW_MESSAGE": "رسالة جديدة",
|
||||
"CALL": "Call",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -457,9 +456,6 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"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": "إسناد لي",
|
||||
@@ -197,6 +196,7 @@
|
||||
"INSERT_READ_MORE": "اقرأ المزيد",
|
||||
"DISMISS_REPLY": "استبعاد الرد",
|
||||
"REPLYING_TO": "الرد على:",
|
||||
"TIP_FORMAT_ICON": "عرض محرر النصوص",
|
||||
"TIP_EMOJI_ICON": "إظهار قائمة الرموز التعبيرية",
|
||||
"TIP_ATTACH_ICON": "إرفاق الملفات",
|
||||
"TIP_AUDIORECORDER_ICON": "تسجيل الصوت",
|
||||
|
||||
@@ -57,13 +57,6 @@
|
||||
"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، الرجاء المحاولة مرة أخرى",
|
||||
@@ -388,11 +381,7 @@
|
||||
"API": {
|
||||
"ERROR_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"
|
||||
"FINISH_MESSAGE": "بدء إعادة توجيه رسائل البريد الإلكتروني الخاصة بك إلى عنوان البريد الإلكتروني التالي."
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "قناة LINE",
|
||||
@@ -478,10 +467,6 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -722,7 +707,6 @@
|
||||
"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.",
|
||||
@@ -1020,7 +1004,6 @@
|
||||
"LINE": "Line",
|
||||
"API": "قناة API",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,6 @@
|
||||
"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",
|
||||
@@ -307,8 +306,6 @@
|
||||
"SETTINGS": "الإعدادات",
|
||||
"CONTACTS": "جهات الاتصال",
|
||||
"ACTIVE": "مفعل",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "قائد",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -401,44 +398,19 @@
|
||||
"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.",
|
||||
"REFRESH_CREDITS": "تحديث"
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
},
|
||||
"CHAT_WITH_US": {
|
||||
"TITLE": "تحتاج مساعدة؟",
|
||||
"DESCRIPTION": "هل تواجه أي مشاكل في الفواتير؟ نحن هنا للمساعدة.",
|
||||
"BUTTON_TXT": "تحدث الينا"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"NO_BILLING_USER": "حساب الفوترة الخاص بك قيد الإعداد. الرجاء تحديث الصفحة وحاول مرة أخرى."
|
||||
},
|
||||
"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.",
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"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,10 +129,6 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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,8 +18,7 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "Call",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -457,9 +456,6 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"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",
|
||||
@@ -197,6 +196,7 @@
|
||||
"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,13 +57,6 @@
|
||||
"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",
|
||||
@@ -388,11 +381,7 @@
|
||||
"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_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"
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "LINE Channel",
|
||||
@@ -478,10 +467,6 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -722,7 +707,6 @@
|
||||
"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.",
|
||||
@@ -1020,7 +1004,6 @@
|
||||
"LINE": "Line",
|
||||
"API": "API Channel",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,6 @@
|
||||
"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",
|
||||
@@ -307,8 +306,6 @@
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Contacts",
|
||||
"ACTIVE": "Active",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -401,44 +398,19 @@
|
||||
"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.",
|
||||
"REFRESH_CREDITS": "Refresh"
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
},
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
},
|
||||
"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.",
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"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,10 +129,6 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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,8 +18,7 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "Ново съобщение",
|
||||
"CALL": "Call",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -457,9 +456,6 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"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",
|
||||
@@ -197,6 +196,7 @@
|
||||
"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,13 +57,6 @@
|
||||
"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",
|
||||
@@ -388,11 +381,7 @@
|
||||
"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_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"
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "LINE Channel",
|
||||
@@ -478,10 +467,6 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -722,7 +707,6 @@
|
||||
"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.",
|
||||
@@ -1020,7 +1004,6 @@
|
||||
"LINE": "Line",
|
||||
"API": "API Channel",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,6 @@
|
||||
"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",
|
||||
@@ -307,8 +306,6 @@
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Контакти",
|
||||
"ACTIVE": "Активен",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -401,44 +398,19 @@
|
||||
"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.",
|
||||
"REFRESH_CREDITS": "Refresh"
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
},
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
},
|
||||
"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.",
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"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,10 +129,6 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Enable regex validation"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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,8 +18,7 @@
|
||||
"CREATED_AT_LABEL": "Created",
|
||||
"NEW_MESSAGE": "New message",
|
||||
"CALL": "Call",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -457,9 +456,6 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"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",
|
||||
@@ -197,6 +196,7 @@
|
||||
"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,13 +57,6 @@
|
||||
"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",
|
||||
@@ -388,11 +381,7 @@
|
||||
"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_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"
|
||||
"FINISH_MESSAGE": "Start forwarding your emails to the following email address."
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "LINE Channel",
|
||||
@@ -478,10 +467,6 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -722,7 +707,6 @@
|
||||
"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.",
|
||||
@@ -1020,7 +1004,6 @@
|
||||
"LINE": "Line",
|
||||
"API": "API Channel",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,6 @@
|
||||
"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",
|
||||
@@ -307,8 +306,6 @@
|
||||
"SETTINGS": "Settings",
|
||||
"CONTACTS": "Contacts",
|
||||
"ACTIVE": "Active",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -401,44 +398,19 @@
|
||||
"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.",
|
||||
"REFRESH_CREDITS": "Refresh"
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
},
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"NO_BILLING_USER": "Your billing account is being configured. Please refresh the page and try again."
|
||||
},
|
||||
"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.",
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
{
|
||||
"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,10 +129,6 @@
|
||||
"ENABLE_REGEX": {
|
||||
"LABEL": "Activa la validació d'expressions regulars"
|
||||
}
|
||||
},
|
||||
"BADGES": {
|
||||
"PRE_CHAT": "Pre-chat",
|
||||
"RESOLUTION": "Resolution"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +102,6 @@
|
||||
},
|
||||
"contact": {
|
||||
"CONTENT": "Shared contact"
|
||||
},
|
||||
"embed": {
|
||||
"CONTENT": "Embedded content"
|
||||
}
|
||||
},
|
||||
"CHAT_SORT_BY_FILTER": {
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"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,8 +18,7 @@
|
||||
"CREATED_AT_LABEL": "Creat",
|
||||
"NEW_MESSAGE": "Nou missatge",
|
||||
"CALL": "Call",
|
||||
"CALL_INITIATED": "Calling the contact…",
|
||||
"CALL_FAILED": "Unable to start the call. Please try again.",
|
||||
"CALL_UNDER_DEVELOPMENT": "Calling is under development",
|
||||
"VOICE_INBOX_PICKER": {
|
||||
"TITLE": "Choose a voice inbox"
|
||||
},
|
||||
@@ -457,9 +456,6 @@
|
||||
"INSTAGRAM": {
|
||||
"PLACEHOLDER": "Add Instagram"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"PLACEHOLDER": "Add TikTok"
|
||||
},
|
||||
"LINKEDIN": {
|
||||
"PLACEHOLDER": "Add LinkedIn"
|
||||
},
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
"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",
|
||||
@@ -197,6 +196,7 @@
|
||||
"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,13 +57,6 @@
|
||||
"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",
|
||||
@@ -388,11 +381,7 @@
|
||||
"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_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"
|
||||
"FINISH_MESSAGE": "Comença a reenviar els teus correus electrònics a la següent adreça electrònica."
|
||||
},
|
||||
"LINE_CHANNEL": {
|
||||
"TITLE": "Canal LINE",
|
||||
@@ -478,10 +467,6 @@
|
||||
"TITLE": "Instagram",
|
||||
"DESCRIPTION": "Connect your instagram account"
|
||||
},
|
||||
"TIKTOK": {
|
||||
"TITLE": "TikTok",
|
||||
"DESCRIPTION": "Connect your TikTok account"
|
||||
},
|
||||
"VOICE": {
|
||||
"TITLE": "Voice",
|
||||
"DESCRIPTION": "Integrate with Twilio Voice"
|
||||
@@ -722,7 +707,6 @@
|
||||
"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.",
|
||||
@@ -1020,7 +1004,6 @@
|
||||
"LINE": "Line",
|
||||
"API": "Canal de l'API",
|
||||
"INSTAGRAM": "Instagram",
|
||||
"TIKTOK": "TikTok",
|
||||
"VOICE": "Voice"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,6 @@
|
||||
"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",
|
||||
@@ -307,8 +306,6 @@
|
||||
"SETTINGS": "Configuracions",
|
||||
"CONTACTS": "Contactes",
|
||||
"ACTIVE": "Actiu",
|
||||
"COMPANIES": "Companies",
|
||||
"ALL_COMPANIES": "All Companies",
|
||||
"CAPTAIN": "Captain",
|
||||
"CAPTAIN_ASSISTANTS": "Assistants",
|
||||
"CAPTAIN_DOCUMENTS": "Documents",
|
||||
@@ -401,44 +398,19 @@
|
||||
"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.",
|
||||
"REFRESH_CREDITS": "Actualitza"
|
||||
"UPGRADE": "Captain is not available on the free plan, upgrade now to get access to assistants, copilot and more."
|
||||
},
|
||||
"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.",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"NO_BILLING_USER": "El teu compte de facturació s'està configurant. Actualitza la pàgina i torna-ho a provar."
|
||||
},
|
||||
"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.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user