Compare commits

...
Author SHA1 Message Date
Muhsin Keloth f2171ffa76 fix: Retry message 2023-10-19 16:47:18 +05:30
Muhsin KelothandGitHub b94c89ebf1 fix: Show sent status for messenger channel (#8145) 2023-10-19 13:22:19 +05:30
Muhsin KelothandGitHub 78ce8a4652 feat: Add support for Instagram delivery reports (#8125) 2023-10-18 23:42:34 -07:00
Muhsin KelothandGitHub 04c874fe35 feat: Add delivery reports for API channel (#8116) 2023-10-18 10:11:13 +05:30
Sojan eabe548520 Merge branch 'release/3.2.0' into develop 2023-10-17 17:55:42 -07:00
Sojan f96b54e57f Bump version to 3.2.0 2023-10-17 17:54:38 -07:00
Pranav Raj SandGitHub d3bf828524 chore: Adds hasConversation flag in chatwoot:on-start-conversation event (#8132) 2023-10-17 16:24:00 -07:00
c20415f3ba test: reset Current.user before each example (#8126)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-10-17 15:42:45 -07:00
Pranav Raj SandGitHub 6b2ed4fe79 fix: Add reset password for edit agent screen (#8130) 2023-10-17 12:54:05 -07:00
Shivam MishraandGitHub eca9ca90f1 feat: Add June event for reply to (#8118) 2023-10-17 09:51:28 -07:00
Shivam MishraandGitHub 9018ec9678 test: reset Current.user after spec (#8123) 2023-10-17 18:06:54 +05:30
Shivam MishraandGitHub e55fe33f1b test: fix flaky test by adding margin for time diff (#8121) 2023-10-17 14:20:40 +05:30
3b0788c3be chore: Update translations (#8024)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2023-10-16 19:55:25 -07:00
Vishnu NarayananandGitHub 626e67b859 feat: add autoupdate for cwctl (#8108)
- During cwctl --upgrade, check for the latest version of cwctl from master branch
- Upgrade cwctl if a newer version is found
2023-10-16 19:43:44 -07:00
Nithin David ThomasandGitHub 28db18ebbd chore: Shows articles in dark mode for widget (#8115) 2023-10-16 16:59:15 +05:30
b28721e10b feat: Creates components for Article Search in Reply [CW-2285] (#7957)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-10-16 15:02:02 +05:30
Shivam MishraandGitHub f77db4d814 refactor: disable bubble by flag (#8109) 2023-10-13 21:34:22 +05:30
62d8ec7edb feat: support reply to for incoming messages on facebook (#8076)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-13 16:33:50 +05:30
7b09fa4a03 chore(snyk): Upgrade gems to fix SNYK-RUBY-RACK-1061917 (#8104)
Co-authored-by: snyk-bot <snyk-bot@snyk.io>
2023-10-13 09:24:50 +05:30
Muhsin KelothandGitHub 980013abae feat: Add delivery reports for live chat (#8092) 2023-10-12 20:48:20 +05:30
Shivam MishraandGitHub 5c729ccea5 chore: Add null checks (#8100) 2023-10-12 19:39:19 +05:30
Vishnu NarayananandGitHub ae52ca87aa fix: Handle empty status in conversation controller (#8091)
Fixes CW-2644
2023-10-12 17:11:04 +05:30
Vishnu NarayananandGitHub 415bb23c37 fix: Handle invalid metric in ReportsController (#8086)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>

Fixes CW-2643
2023-10-12 17:01:23 +05:30
Pranav Raj SandGitHub 9e173fca3c fix: Return 404 if conversation is nil (#8098) 2023-10-12 17:00:51 +05:30
Muhsin KelothandGitHub bd918ee506 fix: Change the message status to failed if the Twilio message delivery status is undelivered (#8097) 2023-10-12 15:27:38 +05:30
Shivam MishraandGitHub 7c9884b853 feat: Remove unused prop (#8093) 2023-10-12 12:14:39 +05:30
7ffa669c5c feat: Implement message bubble reply to (#8068)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-11 22:04:12 +05:30
0bc20873f6 feat: Add delivery status for Twilio Channel (#8082)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-11 21:17:46 +05:30
88de3359a5 fix: Long names hide the resolve button (#8083)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-11 13:25:20 +05:30
Sojan JoseandGitHub a3d008da06 chore: Ensure null validation for private attribute in messages (#8085) 2023-10-11 13:16:59 +05:30
cbae95422d feat: Implement reply to for reply editor (#8063)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-10 19:13:12 +05:30
Shivam MishraandGitHub 081c845c56 chore: Remove twitter actions (#8079) 2023-10-10 16:48:58 +05:30
Pranav Raj SandGitHub 1e54d6deb3 fix: Update duplicate ids for dashboard app frame (#8077) 2023-10-10 16:46:32 +05:30
Shivam MishraandGitHub bb5c6a416a fix: insert position for canned responses (#8069) 2023-10-10 10:36:10 +05:30
6a07251007 feat: Add the ability to self-assign conversations in macros (#8048)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-10 09:38:23 +05:30
Sivin VargheseandGitHub 759a66dd21 feat: Add the ability to send attachment in new conversation (#7913) 2023-10-10 09:34:36 +05:30
Vishnu NarayananandGitHub 8189dd932c feat: Report cwctl events to hub (#8009) 2023-10-10 09:16:03 +05:30
b6ba0f343e feat: Reverse the contact merge (#8057)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-10 08:20:48 +05:30
Sivin VargheseandGitHub 86ca90aa15 feat: Support image resize in message signature (#8042) 2023-10-09 13:20:12 +05:30
Muhsin KelothandGitHub 26e8877cd9 feat: Support link unfurling for all the channels within the same connected channel account. (#8033) 2023-10-08 17:55:03 +05:30
1b63adfb2e feat: add option for reply to in context menu (#8043)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-05 20:01:20 +05:30
e27274a5a8 chore: Adds a bus event to insert text at cursor in editor (#7968)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-10-05 14:37:01 +05:30
e5c198f839 refactor: Remove redundant accounts/get (#8056)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
2023-10-05 13:54:04 +05:30
Sojan JoseandGitHub 72ba429159 fix: Show sources in bot response only if documents are present (#8055)
- show sources section in bot only based on appropriate conditions
2023-10-05 00:11:01 -07:00
Pranav Raj SandGitHub 3227cf7ff3 fix: Update query to support multiple tag conditions (#8054) 2023-10-05 10:47:42 +05:30
f0740120a8 chore: Add a fix for mouse up event in modal (#7950)
Co-authored-by: BikashSah999 <51731962+BikashSah999@users.noreply.github.com>
Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
2023-10-05 10:44:31 +05:30
3ea54065b1 feat: Upgrade prompt for help center (#8010)
Co-authored-by: Sojan Jose <sojan@pepalo.com>
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
2023-10-05 08:34:21 +05:30
Sojan JoseandGitHub 6f19546c3c feat: Add article messages along with bot responses (#7993)
ref: https://linear.app/chatwoot/issue/CW-2464/bot-should-also-return-links-to-the-information
2023-10-04 15:40:59 -07:00
473 changed files with 7039 additions and 1753 deletions
+6 -6
View File
@@ -15,7 +15,7 @@ gem 'browser'
gem 'hashie'
gem 'jbuilder'
gem 'kaminari'
gem 'responders'
gem 'responders', '>= 3.1.1'
gem 'rest-client'
gem 'telephone_number'
gem 'time_diff'
@@ -67,7 +67,7 @@ gem 'webpacker'
gem 'barnes'
##--- gems for authentication & authorization ---##
gem 'devise'
gem 'devise', '>= 4.9.3'
gem 'devise-secure_password', git: 'https://github.com/chatwoot/devise-secure_password', branch: 'chatwoot'
gem 'devise_token_auth'
# authorization
@@ -92,7 +92,7 @@ gem 'twitty', '~> 0.1.5'
# facebook client
gem 'koala'
# slack client
gem 'slack-ruby-client', '~> 2.0.0'
gem 'slack-ruby-client', '~> 2.2.0'
# for dialogflow integrations
gem 'google-cloud-dialogflow-v2'
gem 'grpc'
@@ -109,9 +109,9 @@ gem 'elastic-apm', require: false
gem 'newrelic_rpm', require: false
gem 'newrelic-sidekiq-metrics', require: false
gem 'scout_apm', require: false
gem 'sentry-rails', '>= 5.11.0', require: false
gem 'sentry-rails', '>= 5.12.0', require: false
gem 'sentry-ruby', require: false
gem 'sentry-sidekiq', '>= 5.11.0', require: false
gem 'sentry-sidekiq', '>= 5.12.0', require: false
##-- background job processing --##
gem 'sidekiq', '>= 7.1.3'
@@ -154,7 +154,7 @@ gem 'stripe'
gem 'faker'
# Include logrange conditionally in intializer using env variable
gem 'lograge', '~> 0.13.0', require: false
gem 'lograge', '~> 0.14.0', require: false
# worked with microsoft refresh token
gem 'omniauth-oauth2'
+20 -21
View File
@@ -148,7 +148,7 @@ GEM
barnes (0.0.9)
multi_json (~> 1)
statsd-ruby (~> 1.1)
bcrypt (3.1.18)
bcrypt (3.1.19)
bindex (0.8.1)
blingfire (0.1.8)
bootsnap (1.16.0)
@@ -193,7 +193,7 @@ GEM
irb (>= 1.5.0)
reline (>= 0.3.1)
declarative (0.0.20)
devise (4.9.2)
devise (4.9.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0)
@@ -273,7 +273,7 @@ GEM
googleauth (~> 1.0)
grpc (~> 1.36)
geocoder (1.8.1)
gli (2.21.0)
gli (2.21.1)
globalid (1.2.1)
activesupport (>= 6.1)
gmail_xoauth (0.4.2)
@@ -434,12 +434,12 @@ GEM
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
lograge (0.13.0)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
railties (>= 4)
request_store (~> 1.0)
loofah (2.21.3)
loofah (2.21.4)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.8.1)
@@ -616,7 +616,7 @@ GEM
uber (< 0.2.0)
request_store (1.5.1)
rack (>= 1.4)
responders (3.1.0)
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rest-client (2.1.0)
@@ -702,18 +702,18 @@ GEM
activesupport (>= 4)
selectize-rails (0.12.6)
semantic_range (3.0.0)
sentry-rails (5.11.0)
sentry-rails (5.12.0)
railties (>= 5.0)
sentry-ruby (~> 5.11.0)
sentry-ruby (5.11.0)
sentry-ruby (~> 5.12.0)
sentry-ruby (5.12.0)
concurrent-ruby (~> 1.0, >= 1.0.2)
sentry-sidekiq (5.11.0)
sentry-ruby (~> 5.11.0)
sentry-sidekiq (5.12.0)
sentry-ruby (~> 5.12.0)
sidekiq (>= 3.0)
sexp_processor (4.17.0)
shoulda-matchers (5.3.0)
activesupport (>= 5.2.0)
sidekiq (7.1.3)
sidekiq (7.1.6)
concurrent-ruby (< 2)
connection_pool (>= 2.3.0)
rack (>= 2.2.4)
@@ -732,13 +732,12 @@ GEM
json (>= 1.8, < 3)
simplecov-html (~> 0.10.0)
simplecov-html (0.10.2)
slack-ruby-client (2.0.0)
slack-ruby-client (2.2.0)
faraday (>= 2.0)
faraday-mashify
faraday-multipart
gli
hashie
websocket-driver
snaky_hash (2.0.1)
hashie
version_gem (~> 1.1, >= 1.1.1)
@@ -817,7 +816,7 @@ GEM
working_hours (1.4.1)
activesupport (>= 3.2)
tzinfo
zeitwerk (2.6.11)
zeitwerk (2.6.12)
PLATFORMS
arm64-darwin-20
@@ -856,7 +855,7 @@ DEPENDENCIES
database_cleaner
ddtrace
debug (~> 1.8)
devise
devise (>= 4.9.3)
devise-secure_password!
devise_token_auth
dotenv-rails
@@ -892,7 +891,7 @@ DEPENDENCIES
line-bot-api
liquid
listen
lograge (~> 0.13.0)
lograge (~> 0.14.0)
maxminddb
mock_redis
neighbor
@@ -916,7 +915,7 @@ DEPENDENCIES
rails (~> 7.0.8.0)
redis
redis-namespace
responders
responders (>= 3.1.1)
rest-client
reverse_markdown
rspec-rails
@@ -928,14 +927,14 @@ DEPENDENCIES
scout_apm
scss_lint
seed_dump
sentry-rails (>= 5.11.0)
sentry-rails (>= 5.12.0)
sentry-ruby
sentry-sidekiq (>= 5.11.0)
sentry-sidekiq (>= 5.12.0)
shoulda-matchers
sidekiq (>= 7.1.3)
sidekiq-cron (>= 1.10.1)
simplecov (= 0.17.1)
slack-ruby-client (~> 2.0.0)
slack-ruby-client (~> 2.2.0)
spring
spring-watcher-listen
squasher
+1 -1
View File
@@ -1 +1 @@
2.4.0
2.6.0
@@ -93,6 +93,9 @@ class Messages::Facebook::MessageBuilder < Messages::Messenger::MessageBuilder
message_type: @message_type,
content: response.content,
source_id: response.identifier,
content_attributes: {
in_reply_to_external_id: response.in_reply_to_external_id
},
sender: @outgoing_echo ? nil : @contact_inbox.contact
}
end
+14 -1
View File
@@ -15,7 +15,10 @@ class V2::ReportBuilder
end
def timeseries
send(params[:metric])
return send(params[:metric]) if metric_valid?
Rails.logger.error "ReportBuilder: Invalid metric - #{params[:metric]}"
{}
end
# For backward compatible with old report
@@ -53,6 +56,16 @@ class V2::ReportBuilder
private
def metric_valid?
%w[conversations_count
incoming_messages_count
outgoing_messages_count
avg_first_response_time
avg_resolution_time reply_time
resolutions_count
reply_time].include?(params[:metric])
end
def inbox
@inbox ||= account.inboxes.find(params[:id])
end
@@ -9,7 +9,6 @@ class Api::V1::Accounts::Actions::ContactMergesController < Api::V1::Accounts::B
mergee_contact: @mergee_contact
)
contact_merge_action.perform
render json: @base_contact
end
private
@@ -60,7 +60,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end
def toggle_status
if params[:status]
if params[:status].present?
set_conversation_status
@status = @conversation.save!
else
@@ -1,5 +1,6 @@
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
include Events::Types
before_action :render_not_found_if_empty, only: [:toggle_typing, :toggle_status, :set_custom_attributes, :destroy_custom_attributes]
def index
@conversation = conversation
@@ -27,6 +28,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
conversation.contact_last_seen_at = DateTime.now.utc
conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(conversation)
head :ok
end
@@ -41,8 +43,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def toggle_typing
head :ok && return if conversation.nil?
case permitted_params[:typing_status]
when 'on'
trigger_typing_event(CONVERSATION_TYPING_ON)
@@ -54,8 +54,6 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
end
def toggle_status
return head :not_found if conversation.nil?
return head :forbidden unless @web_widget.end_conversation?
unless conversation.resolved?
@@ -81,6 +79,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
end
def render_not_found_if_empty
return head :not_found if conversation.nil?
end
def permitted_params
params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number],
message: [:content, :referer_url, :timestamp, :echo_id],
@@ -23,6 +23,7 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
def update_last_seen
@conversation.contact_last_seen_at = DateTime.now.utc
@conversation.save!
::Conversations::MarkMessagesAsReadJob.perform_later(@conversation)
head :ok
end
@@ -0,0 +1,21 @@
class Twilio::DeliveryStatusController < ApplicationController
def create
::Twilio::DeliveryStatusService.new(params: permitted_params).perform
head :no_content
end
private
def permitted_params
params.permit(
:AccountSid,
:From,
:MessageSid,
:MessagingServiceSid,
:MessageStatus,
:ErrorCode,
:ErrorMessage
)
end
end
+2 -2
View File
@@ -1,13 +1,13 @@
<template>
<div
v-if="!authUIFlags.isFetching"
v-if="!authUIFlags.isFetching && !accountUIFlags.isFetchingItem"
id="app"
class="app-wrapper h-full flex-grow-0 min-h-0 w-full"
:class="{ 'app-rtl--wrapper': isRTLView }"
:dir="isRTLView ? 'rtl' : 'ltr'"
>
<update-banner :latest-chatwoot-version="latestChatwootVersion" />
<template v-if="!accountUIFlags.isFetchingItem && currentAccountId">
<template v-if="currentAccountId">
<payment-pending-banner />
<upgrade-banner />
</template>
+5
View File
@@ -84,6 +84,11 @@ export default {
return axios.delete(endPoints('deleteAvatar').url);
},
resetPassword({ email }) {
const urlData = endPoints('resetPassword');
return axios.post(urlData.url, { email });
},
setActiveAccount({ accountId }) {
const urlData = endPoints('setActiveAccount');
return axios.put(urlData.url, {
@@ -34,26 +34,26 @@
}
&.hollow {
@apply border border-slate-200 dark:border-slate-600 text-woot-700 dark:text-woot-100 hover:bg-woot-50 dark:hover:bg-woot-900;
@apply border border-woot-500 dark:border-woot-500 text-woot-500 dark:text-woot-500 hover:bg-woot-50 dark:hover:bg-woot-900;
&.secondary {
@apply text-slate-700 border-slate-200 dark:border-slate-600 dark:text-slate-100 hover:bg-slate-50 dark:hover:bg-slate-700;
}
&.success {
@apply text-green-700 dark:text-green-100 hover:bg-green-50 dark:hover:bg-green-800;
@apply text-green-700 dark:text-green-400 border-green-100 dark:border-green-600 hover:bg-green-50 dark:hover:bg-green-800;
}
&.alert {
@apply text-red-700 dark:text-red-100 hover:bg-red-50 dark:hover:bg-red-800;
@apply text-red-700 dark:text-red-400 border-red-100 dark:border-red-600 hover:bg-red-50 dark:hover:bg-red-800;
}
&.warning {
@apply text-yellow-700 dark:text-yellow-100 hover:bg-yellow-50 dark:hover:bg-yellow-800;
@apply text-yellow-600 dark:text-yellow-600 border-yellow-600 dark:border-yellow-700 hover:bg-yellow-50 dark:hover:bg-yellow-800;
}
&:hover {
@apply bg-slate-75 dark:bg-slate-900 border-slate-100 dark:border-slate-700;
@apply bg-woot-75 dark:bg-woot-800 border-slate-100 dark:border-woot-600 dark:text-woot-400;
&.secondary {
@apply border-slate-100 dark:border-slate-700 text-slate-800 dark:text-slate-100;
@@ -68,7 +68,7 @@
}
&.warning {
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-700;
@apply border-slate-100 dark:border-slate-700 text-yellow-700 dark:text-yellow-500;
}
}
}
@@ -152,6 +152,10 @@
&.is-image {
@apply rounded-lg;
.message__mail-head {
@apply px-4 py-2;
}
}
}
@@ -4,10 +4,6 @@
@apply flex items-center justify-center bg-modal dark:bg-modal z-[9990] h-full left-0 fixed top-0 w-full;
}
.modal--close {
@apply absolute right-2 rtl:right-[unset] rtl:left-2 top-2;
}
.page-top-bar {
@apply px-8 pt-9 pb-0;
+23 -5
View File
@@ -4,15 +4,19 @@
v-if="show"
:class="modalClassName"
transition="modal"
@click="onBackDropClick"
@mousedown="handleMouseDown"
>
<div :class="modalContainerClassName" @click.stop>
<div
:class="modalContainerClassName"
@mouse.stop
@mousedown="event => event.stopPropagation()"
>
<woot-button
v-if="showCloseButton"
color-scheme="secondary"
icon="dismiss"
variant="clear"
class="modal--close"
class="absolute ltr:right-2 rtl:left-2 top-2 z-10"
@click="close"
/>
<slot />
@@ -50,6 +54,11 @@ export default {
default: '',
},
},
data() {
return {
mousedDownOnBackdrop: false,
};
},
computed: {
modalContainerClassName() {
let className =
@@ -77,13 +86,22 @@ export default {
this.onClose();
}
});
document.body.addEventListener('mouseup', this.onMouseUp);
},
beforeDestroy() {
document.body.removeEventListener('mouseup', this.onMouseUp);
},
methods: {
handleMouseDown() {
this.mousedDownOnBackdrop = true;
},
close() {
this.onClose();
},
onBackDropClick() {
if (this.closeOnBackdropClick) {
onMouseUp() {
if (this.mousedDownOnBackdrop) {
this.mousedDownOnBackdrop = false;
this.onClose();
}
},
@@ -11,7 +11,6 @@
@key-shortcut-modal="toggleKeyShortcutModal"
@open-notification-panel="openNotificationPanel"
/>
<secondary-sidebar
v-if="showSecondarySidebar"
:class="sidebarClassName"
@@ -114,7 +113,12 @@ export default {
if (!isAvailableForTheUser) {
return false;
}
if (
menuItem.alwaysVisibleOnChatwootInstances &&
!this.isACustomBrandedInstance
) {
return true;
}
if (menuItem.featureFlag) {
return this.isFeatureEnabledonAccount(
this.accountId,
@@ -42,6 +42,7 @@ const primaryMenuItems = accountId => [
key: 'helpcenter',
label: 'HELP_CENTER.TITLE',
featureFlag: FEATURE_FLAGS.HELP_CENTER,
alwaysVisibleOnChatwootInstances: true,
toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles',
roles: ['administrator'],
@@ -1,11 +1,9 @@
<template>
<div
class="preview-item__wrap flex flex-col overflow-auto mt-4 max-h-[12.5rem]"
>
<div class="preview-item__wrap flex overflow-auto max-h-[12.5rem]">
<div
v-for="(attachment, index) in attachments"
:key="attachment.id"
class="preview-item flex p-1 bg-slate-50 dark:bg-slate-800 rounded-md w-[15rem] mb-1"
class="preview-item flex items-center p-1 bg-slate-50 dark:bg-slate-800 gap-1 rounded-md w-[15rem] mb-1"
>
<div class="max-w-[4rem] flex-shrink-0 w-6 flex items-center">
<img
@@ -17,7 +15,7 @@
📄
</span>
</div>
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis ml-2">
<div class="max-w-[60%] min-w-[50%] overflow-hidden text-ellipsis">
<span
class="h-4 overflow-hidden text-sm font-medium text-ellipsis whitespace-nowrap"
>
@@ -12,7 +12,7 @@
/>
<iframe
v-if="configItem.type === 'frame' && configItem.url"
:id="`dashboard-app--frame-${index}`"
:id="getFrameId(index)"
:src="configItem.url"
@load="() => onIframeLoad(index)"
/>
@@ -39,6 +39,10 @@ export default {
type: Boolean,
default: false,
},
position: {
type: Number,
required: true,
},
},
data() {
return {
@@ -82,10 +86,14 @@ export default {
};
},
methods: {
getFrameId(index) {
return `dashboard-app--frame-${this.position}-${index}`;
},
onIframeLoad(index) {
const frameElement = document.getElementById(
`dashboard-app--frame-${index}`
);
// A possible alternative is to use ref instead of document.getElementById
// However, when ref is used together with v-for, the ref you get will be
// an array containing the child components mirroring the data source.
const frameElement = document.getElementById(this.getFrameId(index));
const eventData = { event: 'appContext', data: this.dashboardAppContext };
frameElement.contentWindow.postMessage(JSON.stringify(eventData), '*');
this.iframeLoading = false;
@@ -1,5 +1,5 @@
<template>
<div class="editor-root">
<div ref="editorRoot" class="relative editor-root">
<tag-agents
v-if="showUserMentions && isPrivate"
:search-key="mentionSearchKey"
@@ -23,6 +23,23 @@
@change="onFileChange"
/>
<div ref="editor" />
<div
v-show="isImageNodeSelected && showImageResizeToolbar"
class="absolute shadow-md rounded-[4px] flex gap-1 py-1 px-1 bg-slate-50 dark:bg-slate-700 text-slate-800 dark:text-slate-50"
:style="{
top: toolbarPosition.top,
left: toolbarPosition.left,
}"
>
<button
v-for="size in sizes"
:key="size.name"
class="text-xs font-medium rounded-[4px] border border-solid border-slate-200 dark:border-slate-600 px-1.5 py-0.5 hover:bg-slate-100 dark:hover:bg-slate-800"
@click="setURLWithQueryAndImageSize(size)"
>
{{ size.name }}
</button>
</div>
<slot name="footer" />
</div>
</template>
@@ -41,6 +58,7 @@ import {
suggestionsPlugin,
triggerCharacters,
} from '@chatwoot/prosemirror-schema/src/mentions/plugin';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import TagAgents from '../conversation/TagAgents.vue';
import CannedResponse from '../conversation/CannedResponse.vue';
@@ -48,6 +66,10 @@ import VariableList from '../conversation/VariableList.vue';
import {
appendSignature,
removeSignature,
insertAtCursor,
scrollCursorIntoView,
findNodeToInsertImage,
setURLWithQueryAndSize,
} from 'dashboard/helper/editorHelper';
const TYPING_INDICATOR_IDLE_TIME = 4000;
@@ -67,8 +89,10 @@ import { CONVERSATION_EVENTS } from '../../../helper/AnalyticsHelper/events';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import alertMixin from 'shared/mixins/alertMixin';
import { findNodeToInsertImage } from 'dashboard/helper/messageEditorHelper';
import { MESSAGE_EDITOR_MENU_OPTIONS } from 'dashboard/constants/editor';
import {
MESSAGE_EDITOR_MENU_OPTIONS,
MESSAGE_EDITOR_IMAGE_RESIZES,
} from 'dashboard/constants/editor';
const createState = (
content,
@@ -111,6 +135,7 @@ export default {
// allowSignature is a kill switch, ensuring no signature methods
// are triggered except when this flag is true
allowSignature: { type: Boolean, default: false },
showImageResizeToolbar: { type: Boolean, default: false }, // A kill switch to show or hide the image toolbar
},
data() {
return {
@@ -123,9 +148,16 @@ export default {
editorView: null,
range: null,
state: undefined,
isImageNodeSelected: false,
toolbarPosition: { top: 0, left: 0 },
sizes: MESSAGE_EDITOR_IMAGE_RESIZES,
selectedImageNode: null,
};
},
computed: {
editorRoot() {
return this.$refs.editorRoot;
},
contentFromEditor() {
return MessageMarkdownSerializer.serialize(this.editorView.state.doc);
},
@@ -273,6 +305,7 @@ export default {
const tr = this.editorView.state.tr.replaceSelectionWith(node);
this.editorView.focus();
this.state = this.editorView.state.apply(tr);
this.editorView.updateState(this.state);
this.emitOnChange();
this.$emit('clear-selection');
}
@@ -298,7 +331,17 @@ export default {
mounted() {
this.createEditorView();
this.editorView.updateState(this.state);
this.focusEditor(this.value);
this.focusEditorInputField();
// BUS Event to insert text or markdown into the editor at the
// current cursor position.
// Components using this
// 1. SearchPopover.vue
bus.$on(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, this.insertContentIntoEditor);
},
beforeDestroy() {
bus.$off(BUS_EVENTS.INSERT_INTO_RICH_EDITOR, this.insertContentIntoEditor);
},
methods: {
reloadState(content = this.value) {
@@ -385,6 +428,7 @@ export default {
state: this.state,
dispatchTransaction: tx => {
this.state = this.state.apply(tx);
this.editorView.updateState(this.state);
this.emitOnChange();
},
handleDOMEvents: {
@@ -397,6 +441,9 @@ export default {
focus: () => {
this.onFocus();
},
click: () => {
// this.isEditorMouseFocusedOnAnImage(); Enable it when the backend supports for message resize is done.
},
blur: () => {
this.onBlur();
},
@@ -409,6 +456,50 @@ export default {
},
});
},
isEditorMouseFocusedOnAnImage() {
if (!this.showImageResizeToolbar) {
return;
}
this.selectedImageNode = document.querySelector(
'img.ProseMirror-selectednode'
);
if (this.selectedImageNode) {
this.isImageNodeSelected = !!this.selectedImageNode;
// Get the position of the selected node
this.setToolbarPosition();
} else {
this.isImageNodeSelected = false;
}
},
setToolbarPosition() {
const editorRect = this.editorRoot.getBoundingClientRect();
const rect = this.selectedImageNode.getBoundingClientRect();
this.toolbarPosition = {
top: `${rect.top - editorRect.top - 30}px`,
left: `${rect.left - editorRect.left - 4}px`,
};
},
setURLWithQueryAndImageSize(size) {
if (!this.showImageResizeToolbar) {
return;
}
setURLWithQueryAndSize(this.selectedImageNode, size, this.editorView);
this.isImageNodeSelected = false;
},
updateImgToolbarOnDelete() {
// check if the selected node is present or not on keyup
// this is needed because the user can select an image and then delete it
// in that case, the selected node will be null and we need to hide the toolbar
// otherwise, the toolbar will be visible even when the image is deleted and cause some errors
if (this.selectedImageNode) {
const hasImgSelectedNode = document.querySelector(
'img.ProseMirror-selectednode'
);
if (!hasImgSelectedNode) {
this.isImageNodeSelected = false;
}
}
},
isEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
},
@@ -441,11 +532,7 @@ export default {
userFullName: mentionItem.name,
});
const tr = this.editorView.state.tr
.replaceWith(this.range.from, this.range.to, node)
.insertText(` `);
this.state = this.editorView.state.apply(tr);
this.emitOnChange();
this.insertNodeIntoEditor(node, this.range.from, this.range.to);
this.$track(CONVERSATION_EVENTS.USED_MENTIONS);
return false;
@@ -455,30 +542,22 @@ export default {
message: cannedItem,
variables: this.variables,
});
if (!this.editorView) {
return null;
}
let from = this.range.from - 1;
let node = new MessageMarkdownTransformer(messageSchema).parse(
updatedMessage
);
if (node.textContent === updatedMessage) {
node = this.editorView.state.schema.text(updatedMessage);
from = this.range.from;
}
const from =
node.textContent === updatedMessage
? this.range.from
: this.range.from - 1;
const tr = this.editorView.state.tr.replaceWith(
from,
this.range.to,
node
);
this.insertNodeIntoEditor(node, from, this.range.to);
this.state = this.editorView.state.apply(tr);
this.emitOnChange();
tr.scrollIntoView();
this.$track(CONVERSATION_EVENTS.INSERTED_A_CANNED_RESPONSE);
return false;
},
@@ -486,23 +565,14 @@ export default {
if (!this.editorView) {
return null;
}
let node = this.editorView.state.schema.text(`{{${variable}}}`);
const from = this.range.from;
const tr = this.editorView.state.tr.replaceWith(
from,
this.range.to,
node
);
const content = `{{${variable}}}`;
let node = this.editorView.state.schema.text(content);
const { from, to } = this.range;
this.state = this.editorView.state.apply(tr);
this.emitOnChange();
// The `{{ }}` are added to the message, but the cursor is placed
// and onExit of suggestionsPlugin is not called. So we need to manually hide
this.insertNodeIntoEditor(node, from, to);
this.showVariables = false;
this.$track(CONVERSATION_EVENTS.INSERTED_A_VARIABLE);
tr.scrollIntoView();
return false;
},
openFileBrowser() {
@@ -558,8 +628,6 @@ export default {
},
emitOnChange() {
this.editorView.updateState(this.state);
this.$emit('input', this.contentFromEditor);
},
@@ -602,6 +670,7 @@ export default {
() => this.resetTyping(),
TYPING_INDICATOR_IDLE_TIME
);
// this.updateImgToolbarOnDelete(); Enable it when the backend supports for message resize is done.
},
onKeydown(event) {
if (this.isEnterToSendEnabled()) {
@@ -619,6 +688,19 @@ export default {
onFocus() {
this.$emit('focus');
},
insertContentIntoEditor(content, defaultFrom = 0) {
const from = defaultFrom || this.editorView.state.selection.from || 0;
let node = new MessageMarkdownTransformer(messageSchema).parse(content);
this.insertNodeIntoEditor(node, from, undefined);
},
insertNodeIntoEditor(node, from = 0, to = 0) {
this.state = insertAtCursor(this.editorView, node, from, to);
this.emitOnChange();
this.$nextTick(() => {
scrollCursorIntoView(this.editorView);
});
},
},
};
</script>
@@ -712,6 +794,6 @@ export default {
}
.editor-warning__message {
@apply text-red-400 dark:text-red-400 text-sm font-normal pt-1 pb-0 px-0;
@apply text-red-400 dark:text-red-400 font-normal text-sm pt-1 pb-0 px-0;
}
</style>
@@ -14,10 +14,11 @@
<file-upload
ref="upload"
v-tooltip.top-end="$t('CONVERSATION.REPLYBOX.TIP_ATTACH_ICON')"
input-id="conversationAttachment"
:size="4096 * 4096"
:accept="allowedFileTypes"
:multiple="enableMultipleFileUpload"
:drop="true"
:drop="enableDragAndDrop"
:drop-directory="false"
:data="{
direct_upload_url: '/rails/active_storage/direct_uploads',
@@ -100,9 +101,9 @@
<transition name="modal-fade">
<div
v-show="$refs.upload && $refs.upload.dropActive"
class="modal-mask"
class="flex items-center justify-center gap-2 fixed left-0 right-0 top-0 bottom-0 w-full h-full z-20 text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col"
>
<fluent-icon icon="cloud-backup" />
<fluent-icon icon="cloud-backup" size="40" />
<h4 class="page-sub-title text-slate-600 dark:text-slate-200">
{{ $t('CONVERSATION.REPLYBOX.DRAG_DROP') }}
</h4>
@@ -228,6 +229,10 @@ export default {
type: String,
default: '',
},
newConversationModalActive: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters({
@@ -274,6 +279,9 @@ export default {
}
return ALLOWED_FILE_TYPES;
},
enableDragAndDrop() {
return !this.newConversationModalActive;
},
audioRecorderPlayStopIcon() {
switch (this.recordingAudioState) {
// playing paused recording stopped inactive destroyed
@@ -346,12 +354,4 @@ export default {
@apply dark:bg-slate-800 bg-slate-100;
}
}
.modal-mask {
@apply text-slate-600 dark:text-slate-200 bg-white_transparent dark:bg-black_transparent flex-col;
}
.icon {
@apply text-[5rem];
}
</style>
@@ -52,6 +52,7 @@
:key="currentChat.id + '-' + dashboardApp.id"
:is-visible="activeIndex - 1 === index"
:config="dashboardApps[index].content"
:position="index"
:current-chat="currentChat"
/>
</div>
@@ -1,6 +1,6 @@
<template>
<div
class="conversation flex flex-shrink-0 flex-grow-0 w-auto max-w-full cursor-pointer relative py-0 px-4 border-transparent border-l-2 border-t-0 border-b-0 border-r-0 border-solid items-start hover:bg-slate-25 dark:hover:bg-slate-800 group"
class="relative flex items-start flex-grow-0 flex-shrink-0 w-auto max-w-full px-4 py-0 border-t-0 border-b-0 border-l-2 border-r-0 border-transparent border-solid cursor-pointer conversation hover:bg-slate-25 dark:hover:bg-slate-800 group"
:class="{
'active bg-slate-25 dark:bg-slate-800 border-woot-500': isActiveChat,
'unread-chat': hasUnread,
@@ -31,7 +31,7 @@
size="40px"
/>
<div
class="py-3 px-0 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
class="px-0 py-3 border-b group-last:border-transparent group-hover:border-transparent border-slate-50 dark:border-slate-800/75 columns"
>
<div class="flex justify-between">
<inbox-name v-if="showInboxName" :inbox="inbox" />
@@ -55,44 +55,11 @@
>
{{ currentContact.name }}
</h4>
<p
<message-preview
v-if="lastMessageInChat"
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
>
<fluent-icon
v-if="isMessagePrivate"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="lock-closed"
/>
<fluent-icon
v-else-if="messageByAgent"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="arrow-reply"
/>
<fluent-icon
v-else-if="isMessageAnActivity"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="info"
/>
<span v-if="lastMessageInChat.content">
{{ parsedLastMessage }}
</span>
<span v-else-if="lastMessageInChat.attachments">
<fluent-icon
v-if="attachmentIcon"
size="16"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
:icon="attachmentIcon"
/>
{{ $t(`${attachmentMessageContent}`) }}
</span>
<span v-else>
{{ $t('CHAT_LIST.NO_CONTENT') }}
</span>
</p>
:message="lastMessageInChat"
class="conversation--message my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] text-sm text-slate-700 dark:text-slate-200"
/>
<p
v-else
class="conversation--message text-slate-700 dark:text-slate-200 text-sm my-0 mx-2 leading-6 h-6 max-w-[96%] w-[16.875rem] overflow-hidden text-ellipsis whitespace-nowrap"
@@ -106,8 +73,8 @@
{{ $t(`CHAT_LIST.NO_MESSAGES`) }}
</span>
</p>
<div class="conversation--meta flex flex-col absolute right-4 top-4">
<span class="text-black-600 text-xxs font-normal leading-4 ml-auto">
<div class="absolute flex flex-col conversation--meta right-4 top-4">
<span class="ml-auto font-normal leading-4 text-black-600 text-xxs">
<time-ago
:last-activity-timestamp="chat.timestamp"
:created-at-timestamp="chat.created_at"
@@ -145,9 +112,8 @@
</template>
<script>
import { mapGetters } from 'vuex';
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import Thumbnail from '../Thumbnail.vue';
import MessagePreview from './MessagePreview.vue';
import conversationMixin from '../../../mixins/conversations';
import timeMixin from '../../../mixins/time';
import router from '../../../routes';
@@ -159,14 +125,6 @@ import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo.vue';
import CardLabels from './conversationCardComponents/CardLabels.vue';
import PriorityMark from './PriorityMark.vue';
const ATTACHMENT_ICONS = {
image: 'image',
audio: 'headphones-sound-wave',
video: 'video',
file: 'document',
location: 'location',
fallback: 'link',
};
export default {
components: {
@@ -175,16 +133,11 @@ export default {
Thumbnail,
ConversationContextMenu,
TimeAgo,
MessagePreview,
PriorityMark,
},
mixins: [
inboxMixin,
timeMixin,
conversationMixin,
messageFormatterMixin,
alertMixin,
],
mixins: [inboxMixin, timeMixin, conversationMixin, alertMixin],
props: {
activeLabel: {
type: String,
@@ -258,20 +211,6 @@ export default {
);
},
lastMessageFileType() {
const lastMessage = this.lastMessageInChat;
const [{ file_type: fileType } = {}] = lastMessage.attachments;
return fileType;
},
attachmentIcon() {
return ATTACHMENT_ICONS[this.lastMessageFileType];
},
attachmentMessageContent() {
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
},
isActiveChat() {
return this.currentChat.id === this.chat.id;
},
@@ -292,30 +231,6 @@ export default {
return this.lastMessage(this.chat);
},
messageByAgent() {
const lastMessage = this.lastMessageInChat;
const { message_type: messageType } = lastMessage;
return messageType === MESSAGE_TYPE.OUTGOING;
},
isMessageAnActivity() {
const lastMessage = this.lastMessageInChat;
const { message_type: messageType } = lastMessage;
return messageType === MESSAGE_TYPE.ACTIVITY;
},
isMessagePrivate() {
const lastMessage = this.lastMessageInChat;
const { private: isPrivate } = lastMessage;
return isPrivate;
},
parsedLastMessage() {
const { content_attributes: contentAttributes } = this.lastMessageInChat;
const { email: { subject } = {} } = contentAttributes || {};
return this.getPlainText(subject || this.lastMessageInChat.content);
},
inbox() {
const { inbox_id: inboxId } = this.chat;
const stateInbox = this.$store.getters['inboxes/getInbox'](inboxId);
@@ -3,10 +3,10 @@
class="bg-white dark:bg-slate-900 flex justify-between items-center py-2 px-4 border-b border-slate-50 dark:border-slate-800/50 flex-col md:flex-row"
>
<div
class="flex-1 w-100 flex flex-col md:flex-row items-center justify-center"
class="flex-1 w-full min-w-0 flex flex-col md:flex-row items-center justify-center"
>
<div
class="flex justify-center items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0"
class="flex justify-start items-center mr-4 rtl:mr-0 rtl:ml-4 min-w-0 w-[inherit]"
>
<back-button
v-if="showBackButton"
@@ -19,26 +19,31 @@
:username="currentContact.name"
:status="currentContact.availability_status"
/>
<div class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0">
<woot-button
variant="link"
color-scheme="secondary"
class="overflow-hidden whitespace-nowrap text-ellipsis"
@click.prevent="$emit('contact-panel-toggle')"
>
<h3
class="text-base inline-block leading-tight capitalize m-0 p-0 text-ellipsis overflow-hidden whitespace-nowrap text-slate-900 dark:text-slate-100"
<div
class="items-start flex flex-col ml-2 rtl:ml-0 rtl:mr-2 min-w-0 w-[inherit] overflow-hidden"
>
<div class="flex items-center flex-row gap-1 m-0 p-0 w-[inherit]">
<woot-button
variant="link"
color-scheme="secondary"
class="[&>span]:overflow-hidden [&>span]:whitespace-nowrap [&>span]:text-ellipsis min-w-0"
@click.prevent="$emit('contact-panel-toggle')"
>
<span>{{ currentContact.name }}</span>
<fluent-icon
v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
size="14"
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0.5"
icon="warning"
/>
</h3>
</woot-button>
<span
class="text-base leading-tight text-slate-900 dark:text-slate-100"
>
{{ currentContact.name }}
</span>
</woot-button>
<fluent-icon
v-if="!isHMACVerified"
v-tooltip="$t('CONVERSATION.UNVERIFIED_SESSION')"
size="14"
class="text-yellow-600 dark:text-yellow-500 my-0 mx-0 min-w-[14px]"
icon="warning"
/>
</div>
<div
class="conversation--header--actions items-center flex text-xs gap-2 text-ellipsis overflow-hidden whitespace-nowrap"
>
@@ -34,6 +34,11 @@
:url="storyUrl"
/>
</blockquote>
<bubble-reply-to
v-if="inReplyToMessageId && inboxSupportsReplyTo"
:message="inReplyTo"
:message-type="data.message_type"
/>
<bubble-text
v-if="data.content"
:message="message"
@@ -124,6 +129,7 @@
:message="data"
@open="openContextMenu"
@close="closeContextMenu"
@replyTo="handleReplyTo"
/>
</div>
</li>
@@ -140,6 +146,7 @@ import BubbleLocation from './bubble/Location.vue';
import BubbleMailHead from './bubble/MailHead.vue';
import BubbleText from './bubble/Text.vue';
import BubbleContact from './bubble/Contact.vue';
import BubbleReplyTo from './bubble/ReplyTo.vue';
import Spinner from 'shared/components/Spinner.vue';
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
import instagramImageErrorPlaceholder from './instagramImageErrorPlaceholder.vue';
@@ -149,6 +156,8 @@ import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { generateBotMessageContent } from './helpers/botMessageContentHelper';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { ACCOUNT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
export default {
components: {
@@ -162,6 +171,7 @@ export default {
BubbleMailHead,
BubbleText,
BubbleContact,
BubbleReplyTo,
ContextMenu,
Spinner,
instagramImageErrorPlaceholder,
@@ -188,6 +198,14 @@ export default {
type: Boolean,
default: false,
},
inboxSupportsReplyTo: {
type: Boolean,
default: false,
},
inReplyTo: {
type: Object,
default: () => ({}),
},
},
data() {
return {
@@ -264,11 +282,19 @@ export default {
) + botMessageContent
);
},
inReplyToMessageId() {
// Why not use the inReplyTo object directly?
// Glad you asked! The inReplyTo object may or may not be available
// depending on the current scroll position of the message list
// since old messages are only loaded when the user scrolls up
return this.data.content_attributes?.in_reply_to;
},
contextMenuEnabledOptions() {
return {
copy: this.hasText,
delete: this.hasText || this.hasAttachments,
cannedResponse: this.isOutgoing && this.hasText,
replyTo: !this.data.private && this.inboxSupportsReplyTo,
};
},
contentAttributes() {
@@ -318,6 +344,8 @@ export default {
left: isLeftAligned,
right: isRightAligned,
'has-context-menu': this.showContextMenu,
// this handles the offset required to align the context menu button
// extra alignment is required since a tweet message has a the user name and avatar below it
'has-tweet-menu': this.isATweet,
'has-bg': this.showBackgroundHighlight,
};
@@ -464,7 +492,10 @@ export default {
this.showContextMenu = !this.showContextMenu;
},
async retrySendMessage() {
await this.$store.dispatch('sendMessageWithData', this.data);
await this.$store.dispatch('sendMessageWithData', {
conversationId: this.data.conversation_id,
...this.data,
});
},
onImageLoadError() {
this.hasImageError = true;
@@ -494,6 +525,13 @@ export default {
this.showContextMenu = false;
this.contextMenuPosition = { x: null, y: null };
},
handleReplyTo() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
const { conversation_id: conversationId, id: replyTo } = this.data;
LocalStorage.updateJsonStore(replyStorageKey, conversationId, replyTo);
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.data);
},
setupHighlightTimer() {
if (Number(this.$route.query.messageId) !== Number(this.data.id)) {
return;
@@ -613,6 +651,8 @@ li.right {
}
li.left.has-tweet-menu .context-menu {
// this handles the offset required to align the context menu button
// extra alignment is required since a tweet message has a the user name and avatar below it
@apply mb-6;
}
@@ -0,0 +1,93 @@
<template>
<div class="overflow-hidden text-ellipsis whitespace-nowrap">
<template v-if="showMessageType">
<fluent-icon
v-if="isMessagePrivate"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="lock-closed"
/>
<fluent-icon
v-else-if="messageByAgent"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="arrow-reply"
/>
<fluent-icon
v-else-if="isMessageAnActivity"
size="16"
class="-mt-0.5 align-middle text-slate-600 dark:text-slate-300 inline-block"
icon="info"
/>
</template>
<span v-if="message.content">
{{ parsedLastMessage }}
</span>
<span v-else-if="message.attachments">
<fluent-icon
v-if="attachmentIcon && showMessageType"
size="16"
class="-mt-0.5 align-middle inline-block text-slate-600 dark:text-slate-300"
:icon="attachmentIcon"
/>
{{ $t(`${attachmentMessageContent}`) }}
</span>
<span v-else>
{{ defaultEmptyMessage || $t('CHAT_LIST.NO_CONTENT') }}
</span>
</div>
</template>
<script>
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import { ATTACHMENT_ICONS } from 'shared/constants/messages';
export default {
name: 'MessagePreview',
mixins: [messageFormatterMixin],
props: {
message: {
type: Object,
required: true,
},
showMessageType: {
type: Boolean,
default: true,
},
defaultEmptyMessage: {
type: String,
default: '',
},
},
computed: {
messageByAgent() {
const { message_type: messageType } = this.message;
return messageType === MESSAGE_TYPE.OUTGOING;
},
isMessageAnActivity() {
const { message_type: messageType } = this.message;
return messageType === MESSAGE_TYPE.ACTIVITY;
},
isMessagePrivate() {
const { private: isPrivate } = this.message;
return isPrivate;
},
parsedLastMessage() {
const { content_attributes: contentAttributes } = this.message;
const { email: { subject } = {} } = contentAttributes || {};
return this.getPlainText(subject || this.message.content);
},
lastMessageFileType() {
const [{ file_type: fileType } = {}] = this.message.attachments;
return fileType;
},
attachmentIcon() {
return ATTACHMENT_ICONS[this.lastMessageFileType];
},
attachmentMessageContent() {
return `CHAT_LIST.ATTACHMENTS.${this.lastMessageFileType}.CONTENT`;
},
},
};
</script>
@@ -1,5 +1,5 @@
<template>
<div class="m-0 flex flex-col justify-between h-full flex-grow min-w-0">
<div class="flex flex-col justify-between flex-grow h-full min-w-0 m-0">
<banner
v-if="!currentChat.can_reply"
color-scheme="alert"
@@ -7,21 +7,12 @@
:href-link="replyWindowLink"
:href-link-text="replyWindowLinkText"
/>
<banner
v-if="isATweet"
color-scheme="gray"
:banner-message="tweetBannerText"
:has-close-button="hasSelectedTweetId"
@close="removeTweetSelection"
/>
<div class="flex justify-end">
<woot-button
variant="smooth"
size="tiny"
color-scheme="secondary"
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
class="rounded-bl-calc rtl:rotate-180 rounded-tl-calc fixed top-[9.5rem] md:top-[6.25rem] z-10 bg-white dark:bg-slate-700 border-slate-50 dark:border-slate-600 border-solid border border-r-0 box-border"
:icon="isRightOrLeftIcon"
@click="onToggleContactPanel"
/>
@@ -42,6 +33,8 @@
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
/>
<li v-show="unreadMessageCount != 0" class="unread--toast">
<span>
@@ -63,6 +56,8 @@
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory"
:is-web-widget-inbox="isAWebWidgetInbox"
:inbox-supports-reply-to="inboxSupportsReplyTo"
:in-reply-to="getInReplyToMessage(message)"
/>
<conversation-label-suggestion
v-if="shouldShowLabelSuggestions"
@@ -87,8 +82,6 @@
</div>
<reply-box
:conversation-id="currentChat.id"
:is-a-tweet="isATweet"
:selected-tweet="selectedTweet"
:popout-reply-box.sync="isPopoutReplyBox"
@click="showPopoutReplyBox"
/>
@@ -110,7 +103,7 @@ import { mapGetters } from 'vuex';
import conversationMixin, {
filterDuplicateSourceMessages,
} from '../../../mixins/conversations';
import inboxMixin from 'shared/mixins/inboxMixin';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import configMixin from 'shared/mixins/configMixin';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import aiMixin from 'dashboard/mixins/aiMixin';
@@ -123,6 +116,7 @@ import { LocalStorage } from 'shared/helpers/localStorage';
// constants
import { BUS_EVENTS } from 'shared/constants/busEvents';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { REPLY_POLICY } from 'shared/constants/links';
import wootConstants from 'dashboard/constants/globals';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
@@ -153,7 +147,6 @@ export default {
isLoadingPrevious: true,
heightBeforeLoad: null,
conversationPanel: null,
selectedTweetId: null,
hasUserScrolled: false,
isProgrammaticScroll: false,
isPopoutReplyBox: false,
@@ -164,12 +157,14 @@ export default {
computed: {
...mapGetters({
accountId: 'getCurrentAccountId',
currentChat: 'getSelectedChat',
allConversations: 'getAllConversations',
inboxesList: 'inboxes/getInboxes',
listLoadingStatus: 'getAllMessagesLoaded',
loadingChatList: 'getChatListLoadingStatus',
appIntegrations: 'integrations/getAppIntegrations',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
currentAccountId: 'getCurrentAccountId',
}),
isOpen() {
@@ -189,16 +184,6 @@ export default {
inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId);
},
hasSelectedTweetId() {
return !!this.selectedTweetId;
},
tweetBannerText() {
return !this.selectedTweetId
? this.$t('CONVERSATION.SELECT_A_TWEET_TO_REPLY')
: `
${this.$t('CONVERSATION.REPLYING_TO')}
${this.selectedTweet.content}` || '';
},
typingUsersList() {
const userList = this.$store.getters[
'conversationTypingStatus/getUserList'
@@ -257,17 +242,6 @@ export default {
hasInstagramStory() {
return this.conversationType === 'instagram_direct_message';
},
selectedTweet() {
if (this.selectedTweetId) {
const { messages = [] } = this.currentChat;
const [selectedMessage] = messages.filter(
message => message.id === this.selectedTweetId
);
return selectedMessage || {};
}
return '';
},
isRightOrLeftIcon() {
if (this.isContactPanelOpen) {
return 'arrow-chevron-right';
@@ -316,6 +290,15 @@ export default {
unreadMessageCount() {
return this.currentChat.unread_count || 0;
},
inboxSupportsReplyTo() {
return (
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
);
},
},
watch: {
@@ -326,7 +309,6 @@ export default {
this.fetchAllAttachmentsFromCurrentChat();
this.fetchSuggestions();
this.messageSentSinceOpened = false;
this.selectedTweetId = null;
},
},
@@ -334,7 +316,6 @@ export default {
bus.$on(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
// when a new message comes in, we refetch the label suggestions
bus.$on(BUS_EVENTS.FETCH_LABEL_SUGGESTIONS, this.fetchSuggestions);
bus.$on(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
// when a message is sent we set the flag to true this hides the label suggestions,
// until the chat is changed and the flag is reset in the watch for currentChat
bus.$on(BUS_EVENTS.MESSAGE_SENT, () => {
@@ -405,10 +386,6 @@ export default {
},
removeBusListeners() {
bus.$off(BUS_EVENTS.SCROLL_TO_MESSAGE, this.onScrollToMessage);
bus.$off(BUS_EVENTS.SET_TWEET_REPLY, this.setSelectedTweet);
},
setSelectedTweet(tweetId) {
this.selectedTweetId = tweetId;
},
onScrollToMessage({ messageId = '' } = {}) {
this.$nextTick(() => {
@@ -531,8 +508,18 @@ export default {
makeMessagesRead() {
this.$store.dispatch('markMessagesRead', { id: this.currentChat.id });
},
removeTweetSelection() {
this.selectedTweetId = null;
getInReplyToMessage(parentMessage) {
if (!parentMessage) return {};
const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to;
if (!inReplyToMessageId) return {};
return this.currentChat?.messages.find(message => {
if (message.id === inReplyToMessageId) {
return true;
}
return false;
});
},
},
};
@@ -19,6 +19,11 @@
@click="$emit('click')"
/>
<div class="reply-box__top">
<reply-to-message
v-if="shouldShowReplyToMessage"
:message="inReplyTo"
@dismiss="resetReplyToMessage"
/>
<canned-response
v-if="showMentions && hasSlashCommand"
v-on-clickaway="hideMentions"
@@ -86,6 +91,7 @@
</div>
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
<attachment-preview
class="flex-col mt-4"
:attachments="attachedFiles"
:remove-attachment="removeAttachment"
/>
@@ -115,6 +121,7 @@
:toggle-audio-recorder="toggleAudioRecorder"
:toggle-emoji-picker="toggleEmojiPicker"
:message="message"
:new-conversation-modal-active="newConversationModalActive"
@selectWhatsappTemplate="openWhatsappTemplateModal"
@toggle-editor="toggleRichContentEditor"
@replace-text="replaceText"
@@ -141,6 +148,7 @@ import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import CannedResponse from './CannedResponse.vue';
import ReplyToMessage from './ReplyToMessage.vue';
import ResizableTextArea from 'shared/components/ResizableTextArea.vue';
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview.vue';
import ReplyTopPanel from 'dashboard/components/widgets/WootWriter/ReplyTopPanel.vue';
@@ -162,7 +170,7 @@ import {
import WhatsappTemplates from './WhatsappTemplates/Modal.vue';
import { buildHotKeys } from 'shared/helpers/KeyboardHelpers';
import { MESSAGE_MAX_LENGTH } from 'shared/helpers/MessageTypeHelper';
import inboxMixin from 'shared/mixins/inboxMixin';
import inboxMixin, { INBOX_FEATURES } from 'shared/mixins/inboxMixin';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants/globals';
@@ -176,6 +184,10 @@ import {
replaceSignature,
extractTextFromMarkdown,
} from 'dashboard/helper/editorHelper';
import { FEATURE_FLAGS } from 'dashboard/featureFlags';
import { LOCAL_STORAGE_KEYS } from 'dashboard/constants/localStorage';
import { LocalStorage } from 'shared/helpers/localStorage';
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
@@ -183,6 +195,7 @@ export default {
components: {
EmojiInput,
CannedResponse,
ReplyToMessage,
ResizableTextArea,
AttachmentPreview,
ReplyTopPanel,
@@ -204,14 +217,6 @@ export default {
fileUploadMixin,
],
props: {
selectedTweet: {
type: [Object, String],
default: () => ({}),
},
isATweet: {
type: Boolean,
default: false,
},
popoutReplyBox: {
type: Boolean,
default: false,
@@ -220,6 +225,7 @@ export default {
data() {
return {
message: '',
inReplyTo: {},
isFocused: false,
showEmojiPicker: false,
attachedFiles: [],
@@ -241,6 +247,7 @@ export default {
showUserMentions: false,
showCannedMenu: false,
showVariablesMenu: false,
newConversationModalActive: false,
};
},
computed: {
@@ -251,7 +258,19 @@ export default {
lastEmail: 'getLastEmailInSelectedChat',
globalConfig: 'globalConfig/get',
accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
}),
shouldShowReplyToMessage() {
return (
this.inReplyTo?.id &&
!this.isPrivate &&
this.inboxHasFeature(INBOX_FEATURES.REPLY_TO) &&
this.isFeatureEnabledonAccount(
this.accountId,
FEATURE_FLAGS.MESSAGE_REPLY_TO
)
);
},
showRichContentEditor() {
if (this.isOnPrivateNote || this.isRichEditorEnabled) {
return true;
@@ -323,10 +342,7 @@ export default {
return this.maxLength - this.message.length;
},
isReplyButtonDisabled() {
if (this.isATweet && !this.inReplyTo && !this.isOnPrivateNote) {
return true;
}
if (this.isATwitterInbox) return true;
if (this.hasAttachments || this.hasRecordedAudio) return false;
return (
@@ -359,11 +375,6 @@ export default {
if (this.isASmsInbox) {
return MESSAGE_MAX_LENGTH.TWILIO_SMS;
}
if (this.isATwitterInbox) {
if (this.conversationType === 'tweet') {
return MESSAGE_MAX_LENGTH.TWEET - this.replyToUserLength - 2;
}
}
return MESSAGE_MAX_LENGTH.GENERAL;
},
showFileUpload() {
@@ -381,8 +392,6 @@ export default {
let sendMessageText = this.$t('CONVERSATION.REPLYBOX.SEND');
if (this.isPrivate) {
sendMessageText = this.$t('CONVERSATION.REPLYBOX.CREATE');
} else if (this.conversationType === 'tweet') {
sendMessageText = this.$t('CONVERSATION.REPLYBOX.TWEET');
}
const keyLabel = isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter')
? '(⌘ + ↵)'
@@ -416,10 +425,6 @@ export default {
isOnPrivateNote() {
return this.replyType === REPLY_EDITOR_MODES.NOTE;
},
inReplyTo() {
const selectedTweet = this.selectedTweet || {};
return selectedTweet.id;
},
isOnExpandedLayout() {
const {
LAYOUT_TYPES: { CONDENSED },
@@ -437,15 +442,6 @@ export default {
}
return '';
},
replyToUserLength() {
const selectedTweet = this.selectedTweet || {};
const {
sender: {
additional_attributes: { screen_name: screenName = '' } = {},
} = {},
} = selectedTweet;
return screenName ? screenName.length : 0;
},
isMessageEmpty() {
if (!this.message) {
return true;
@@ -526,6 +522,7 @@ export default {
}
this.setCCAndToEmailsFromLastChat();
this.fetchAndSetReplyTo();
},
conversationIdByRoute(conversationId, oldConversationId) {
if (conversationId !== oldConversationId) {
@@ -567,10 +564,28 @@ export default {
500,
true
);
this.fetchAndSetReplyTo();
bus.$on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
// A hacky fix to solve the drag and drop
// Is showing on top of new conversation modal drag and drop
// TODO need to find a better solution
bus.$on(
BUS_EVENTS.NEW_CONVERSATION_MODAL,
this.onNewConversationModalActive
);
},
destroyed() {
document.removeEventListener('paste', this.onPaste);
document.removeEventListener('keydown', this.handleKeyEvents);
bus.$off(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.fetchAndSetReplyTo);
},
beforeDestroy() {
bus.$off(
BUS_EVENTS.NEW_CONVERSATION_MODAL,
this.onNewConversationModalActive
);
},
methods: {
toggleRichContentEditor() {
@@ -753,6 +768,7 @@ export default {
: this.$track(CONVERSATION_EVENTS.SENT_MESSAGE, {
channelType: this.channelType,
signatureEnabled: this.sendWithSignature,
hasReplyTo: !!this.inReplyTo?.id,
});
},
async onSendReply() {
@@ -855,6 +871,7 @@ export default {
}
this.attachedFiles = [];
this.isRecordingAudio = false;
this.resetReplyToMessage();
},
clearEmailField() {
this.ccEmails = '';
@@ -986,8 +1003,10 @@ export default {
sender: this.sender,
};
if (this.inReplyTo) {
messagePayload.contentAttributes = { in_reply_to: this.inReplyTo };
if (this.inReplyTo?.id) {
messagePayload.contentAttributes = {
in_reply_to: this.inReplyTo.id,
};
}
if (this.attachedFiles && this.attachedFiles.length) {
@@ -1060,6 +1079,33 @@ export default {
this.bccEmails = bcc.join(', ');
this.toEmails = to.join(', ');
},
fetchAndSetReplyTo() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
const replyToMessageId = LocalStorage.getFromJsonStore(
replyStorageKey,
this.conversationId
);
this.inReplyTo = this.currentChat?.messages?.find(message => {
if (message.id === replyToMessageId) {
return true;
}
return false;
});
},
resetReplyToMessage() {
const replyStorageKey = LOCAL_STORAGE_KEYS.MESSAGE_REPLY_TO;
LocalStorage.deleteFromJsonStore(replyStorageKey, this.conversationId);
bus.$emit(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE);
},
onNewConversationModalActive(isActive) {
// Issue is if the new conversation modal is open and we drag and drop the file
// then the file is not getting attached to the new conversation modal
// and it is getting attached to the current conversation reply box
// so to fix this we are removing the drag and drop event listener from the current conversation reply box
// When new conversation modal is open
this.newConversationModalActive = isActive;
},
},
};
</script>
@@ -0,0 +1,49 @@
<template>
<div
class="reply-editor bg-slate-50 dark:bg-slate-800 rounded-md py-1 pl-2 pr-1 text-xs tracking-wide mt-2 flex items-center gap-1.5 -mx-2"
>
<fluent-icon class="flex-shrink-0 icon" icon="arrow-reply" size="14" />
<div class="flex-grow gap-1 mt-px text-xs truncate">
{{ $t('CONVERSATION.REPLYBOX.REPLYING_TO') }}
<message-preview
:message="message"
:show-message-type="false"
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
class="inline"
/>
</div>
<woot-button
v-tooltip="$t('CONVERSATION.REPLYBOX.DISMISS_REPLY')"
color-scheme="secondary"
icon="dismiss"
variant="clear"
size="tiny"
class="flex-shrink-0"
@click.stop="$emit('dismiss')"
/>
</div>
</template>
<script>
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
export default {
components: { MessagePreview },
props: {
message: {
type: Object,
required: true,
},
},
};
</script>
<style lang="scss">
// TODO: Remove this
// override for dashboard/assets/scss/widgets/_reply-box.scss
.reply-editor {
.icon {
margin-right: 0px !important;
}
}
</style>
@@ -33,7 +33,7 @@
<woot-button variant="smooth" @click="$emit('resetTemplate')">
{{ $t('WHATSAPP_TEMPLATES.PARSER.GO_BACK_LABEL') }}
</woot-button>
<woot-button @click="sendMessage">
<woot-button type="button" @click="sendMessage">
{{ $t('WHATSAPP_TEMPLATES.PARSER.SEND_MESSAGE_LABEL') }}
</woot-button>
</footer>
@@ -57,17 +57,6 @@
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
/>
<button
v-if="isATweet && (isIncoming || isOutgoing) && sourceId"
@click="onTweetReply"
>
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.REPLY_TO_TWEET')"
icon="arrow-reply"
class="action--icon cursor-pointer"
size="16"
/>
</button>
<a
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
:href="linkToTweet"
@@ -77,7 +66,7 @@
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.VIEW_TWEET_IN_TWITTER')"
icon="open"
class="action--icon cursor-pointer"
class="cursor-pointer action--icon"
size="16"
/>
</a>
@@ -86,7 +75,6 @@
<script>
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import inboxMixin from 'shared/mixins/inboxMixin';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
@@ -213,7 +201,11 @@ export default {
return !!this.sourceId;
}
if (this.isAWhatsAppChannel) {
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox
) {
return this.sourceId && this.isSent;
}
return false;
@@ -223,9 +215,13 @@ export default {
return false;
}
if (this.isAWhatsAppChannel) {
if (this.isAWhatsAppChannel || this.isATwilioChannel) {
return this.sourceId && this.isDelivered;
}
// We will consider messages as delivered for web widget inbox and API inbox if they are sent
if (this.isAWebWidgetInbox || this.isAPIInbox) {
return this.isSent;
}
return false;
},
@@ -239,18 +235,17 @@ export default {
return contactLastSeenAt >= this.createdAt;
}
if (this.isAWhatsAppChannel) {
if (
this.isAWhatsAppChannel ||
this.isATwilioChannel ||
this.isAFacebookInbox
) {
return this.sourceId && this.isRead;
}
return false;
},
},
methods: {
onTweetReply() {
bus.$emit(BUS_EVENTS.SET_TWEET_REPLY, this.id);
},
},
};
</script>
@@ -0,0 +1,41 @@
<template>
<div
class="px-2 py-1.5 -mx-2 rounded-sm min-w-[15rem] mb-2"
:class="{
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50':
messageType === MESSAGE_TYPE.INCOMING,
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
}"
>
<message-preview
:message="message"
:show-message-type="false"
:default-empty-message="$t('CONVERSATION.REPLY_MESSAGE_NOT_FOUND')"
/>
</div>
</template>
<script>
import MessagePreview from 'dashboard/components/widgets/conversation/MessagePreview.vue';
import { MESSAGE_TYPE } from 'shared/constants/messages';
export default {
name: 'ReplyTo',
components: {
MessagePreview,
},
props: {
message: {
type: Object,
required: true,
},
messageType: {
type: Number,
required: true,
},
},
data() {
return { MESSAGE_TYPE };
},
};
</script>
@@ -32,3 +32,22 @@ export const ARTICLE_EDITOR_MENU_OPTIONS = [
'imageUpload',
'code',
];
export const MESSAGE_EDITOR_IMAGE_RESIZES = [
{
name: 'Small',
height: '24px',
},
{
name: 'Medium',
height: '48px',
},
{
name: 'Large',
height: '72px',
},
{
name: 'Original Size',
height: 'auto',
},
];
@@ -28,6 +28,7 @@ export default {
EXPANDED: 'expanded',
},
DOCS_URL: '//www.chatwoot.com/docs/product/',
HELP_CENTER_DOCS_URL: '//www.chatwoot.com/docs/product/others/help-center',
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
SMALL_SCREEN_BREAKPOINT: 1024,
AVAILABILITY_STATUS_KEYS: ['online', 'busy', 'offline'],
@@ -4,4 +4,5 @@ export const LOCAL_STORAGE_KEYS = {
DRAFT_MESSAGES: 'draftMessages',
COLOR_SCHEME: 'color_scheme',
DISMISSED_LABEL_SUGGESTIONS: 'labelSuggestionsDismissed',
MESSAGE_REPLY_TO: 'messageReplyTo',
};
+1
View File
@@ -16,4 +16,5 @@ export const FEATURE_FLAGS = {
TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder',
AUDIT_LOGS: 'audit_logs',
MESSAGE_REPLY_TO: 'message_reply_to',
};
@@ -156,3 +156,128 @@ export function extractTextFromMarkdown(markdown) {
.replace(/\n{2,}/g, '\n') // Remove multiple consecutive newlines (blank lines)
.trim(); // Trim any extra space
}
/**
* Scrolls the editor view into current cursor position
*
* @param {EditorView} view - The Prosemirror EditorView
*
*/
export const scrollCursorIntoView = view => {
// Get the current selection's head position (where the cursor is).
const pos = view.state.selection.head;
// Get the corresponding DOM node for that position.
const domAtPos = view.domAtPos(pos);
const node = domAtPos.node;
// Scroll the node into view.
if (node && node.scrollIntoView) {
node.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
/**
* Returns a transaction that inserts a node into editor at the given position
* Has an optional param 'content' to check if the
*
* @param {Node} node - The prosemirror node that needs to be inserted into the editor
* @param {number} from - Position in the editor where the node needs to be inserted
* @param {number} to - Position in the editor where the node needs to be replaced
*
*/
export function insertAtCursor(editorView, node, from, to) {
if (!editorView) {
return undefined;
}
// This is a workaround to prevent inserting content into new line rather than on the exiting line
// If the node is of type 'doc' and has only one child which is a paragraph,
// then extract its inline content to be inserted as inline.
const isWrappedInParagraph =
node.type.name === 'doc' &&
node.childCount === 1 &&
node.firstChild.type.name === 'paragraph';
if (isWrappedInParagraph) {
node = node.firstChild.content;
}
let tr;
if (to) {
tr = editorView.state.tr.replaceWith(from, to, node).insertText(` `);
} else {
tr = editorView.state.tr.insert(from, node);
}
const state = editorView.state.apply(tr);
editorView.updateState(state);
editorView.focus();
return state;
}
/**
* Determines the appropriate node and position to insert an image in the editor.
*
* Based on the current editor state and the provided image URL, this function finds out the correct node (either
* a standalone image node or an image wrapped in a paragraph) and its respective position in the editor.
*
* 1. If the current node is a paragraph and doesn't contain an image or text, the image is inserted directly into it.
* 2. If the current node isn't a paragraph or it's a paragraph containing text, the image will be wrapped
* in a new paragraph and then inserted.
* 3. If the current node is a paragraph containing an image, the new image will be inserted directly into it.
*
* @param {Object} editorState - The current state of the editor. It provides necessary details like selection, schema, etc.
* @param {string} fileUrl - The URL of the image to be inserted into the editor.
* @returns {Object|null} An object containing details about the node to be inserted and its position. It returns null if no image node can be created.
* @property {Node} node - The ProseMirror node to be inserted (either an image node or a paragraph containing the image).
* @property {number} pos - The position where the new node should be inserted in the editor.
*/
export const findNodeToInsertImage = (editorState, fileUrl) => {
const { selection, schema } = editorState;
const { nodes } = schema;
const currentNode = selection.$from.node();
const {
type: { name: typeName },
content: { size, content },
} = currentNode;
const imageNode = nodes.image.create({ src: fileUrl });
if (!imageNode) return null;
const isInParagraph = typeName === 'paragraph';
const needsNewLine =
!content.some(n => n.type.name === 'image') && size !== 0 ? 1 : 0;
return {
node: isInParagraph ? imageNode : nodes.paragraph.create({}, imageNode),
pos: selection.from + needsNewLine,
};
};
/**
* Set URL with query and size.
*
* @param {Object} selectedImageNode - The current selected node.
* @param {Object} size - The size to set.
* @param {Object} editorView - The editor view.
*/
export function setURLWithQueryAndSize(selectedImageNode, size, editorView) {
if (selectedImageNode) {
// Create and apply the transaction
const tr = editorView.state.tr.setNodeMarkup(
editorView.state.selection.from,
null,
{
src: selectedImageNode.src,
height: size.height,
}
);
if (tr.docChanged) {
editorView.dispatch(tr);
}
}
}
@@ -1,40 +0,0 @@
/**
* Determines the appropriate node and position to insert an image in the editor.
*
* Based on the current editor state and the provided image URL, this function finds out the correct node (either
* a standalone image node or an image wrapped in a paragraph) and its respective position in the editor.
*
* 1. If the current node is a paragraph and doesn't contain an image or text, the image is inserted directly into it.
* 2. If the current node isn't a paragraph or it's a paragraph containing text, the image will be wrapped
* in a new paragraph and then inserted.
* 3. If the current node is a paragraph containing an image, the new image will be inserted directly into it.
*
* @param {Object} editorState - The current state of the editor. It provides necessary details like selection, schema, etc.
* @param {string} fileUrl - The URL of the image to be inserted into the editor.
* @returns {Object|null} An object containing details about the node to be inserted and its position. It returns null if no image node can be created.
* @property {Node} node - The ProseMirror node to be inserted (either an image node or a paragraph containing the image).
* @property {number} pos - The position where the new node should be inserted in the editor.
*/
export const findNodeToInsertImage = (editorState, fileUrl) => {
const { selection, schema } = editorState;
const { nodes } = schema;
const currentNode = selection.$from.node();
const {
type: { name: typeName },
content: { size, content },
} = currentNode;
const imageNode = nodes.image.create({ src: fileUrl });
if (!imageNode) return null;
const isInParagraph = typeName === 'paragraph';
const needsNewLine =
!content.some(n => n.type.name === 'image') && size !== 0 ? 1 : 0;
return {
node: isInParagraph ? imageNode : nodes.paragraph.create({}, imageNode),
pos: selection.from + needsNewLine,
};
};
@@ -5,7 +5,43 @@ import {
replaceSignature,
cleanSignature,
extractTextFromMarkdown,
insertAtCursor,
findNodeToInsertImage,
setURLWithQueryAndSize,
} from '../editorHelper';
import { EditorState } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import { Schema } from 'prosemirror-model';
// Define a basic ProseMirror schema
const schema = new Schema({
nodes: {
doc: { content: 'paragraph+' },
paragraph: {
content: 'text*',
toDOM: () => ['p', 0], // Represents a paragraph as a <p> tag in the DOM.
},
text: {
toDOM: node => node.text, // Represents text as its actual string value.
},
},
});
// Initialize a basic EditorState for testing
const createEditorState = (content = '') => {
if (!content) {
return EditorState.create({
schema,
doc: schema.node('doc', null, [schema.node('paragraph')]),
});
}
return EditorState.create({
schema,
doc: schema.node('doc', null, [
schema.node('paragraph', null, [schema.text(content)]),
]),
});
};
const NEW_SIGNATURE = 'This is a new signature';
@@ -198,3 +234,208 @@ describe('extractTextFromMarkdown', () => {
expect(extractTextFromMarkdown(markdown)).toEqual(expected);
});
});
describe('insertAtCursor', () => {
it('should return undefined if editorView is not provided', () => {
const result = insertAtCursor(undefined, schema.text('Hello'), 0);
expect(result).toBeUndefined();
});
it('should unwrap doc nodes that are wrapped in a paragraph', () => {
const docNode = schema.node('doc', null, [
schema.node('paragraph', null, [schema.text('Hello')]),
]);
const editorState = createEditorState();
const editorView = new EditorView(document.body, { state: editorState });
insertAtCursor(editorView, docNode, 0);
// Check if node was unwrapped and inserted correctly
expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello');
});
it('should insert node without replacing any content if "to" is not provided', () => {
const editorState = createEditorState();
const editorView = new EditorView(document.body, { state: editorState });
insertAtCursor(editorView, schema.text('Hello'), 0);
// Check if node was inserted correctly
expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello');
});
it('should replace content between "from" and "to" with the provided node', () => {
const editorState = createEditorState('ReplaceMe');
const editorView = new EditorView(document.body, { state: editorState });
insertAtCursor(editorView, schema.text('Hello'), 0, 8);
// Check if content was replaced correctly
expect(editorView.state.doc.firstChild.firstChild.text).toBe('Hello Me');
});
});
describe('findNodeToInsertImage', () => {
let mockEditorState;
beforeEach(() => {
mockEditorState = {
selection: {
$from: {
node: jest.fn(() => ({})),
},
from: 0,
},
schema: {
nodes: {
image: {
create: jest.fn(attrs => ({ type: { name: 'image' }, attrs })),
},
paragraph: {
create: jest.fn((_, node) => ({
type: { name: 'paragraph' },
content: [node],
})),
},
},
},
};
});
it('should insert image directly into an empty paragraph', () => {
const mockNode = {
type: { name: 'paragraph' },
content: { size: 0, content: [] },
};
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
const result = findNodeToInsertImage(mockEditorState, 'image-url');
expect(result).toEqual({
node: { type: { name: 'image' }, attrs: { src: 'image-url' } },
pos: 0,
});
});
it('should insert image directly into a paragraph without an image but with other content', () => {
const mockNode = {
type: { name: 'paragraph' },
content: {
size: 1,
content: [
{
type: { name: 'text' },
},
],
},
};
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
mockEditorState.selection.from = 1;
const result = findNodeToInsertImage(mockEditorState, 'image-url');
expect(result).toEqual({
node: { type: { name: 'image' }, attrs: { src: 'image-url' } },
pos: 2, // Because it should insert after the text, on a new line.
});
});
it("should wrap image in a new paragraph when the current node isn't a paragraph", () => {
const mockNode = {
type: { name: 'not-a-paragraph' },
content: { size: 0, content: [] },
};
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
const result = findNodeToInsertImage(mockEditorState, 'image-url');
expect(result.node.type.name).toBe('paragraph');
expect(result.node.content[0].type.name).toBe('image');
expect(result.node.content[0].attrs.src).toBe('image-url');
expect(result.pos).toBe(0);
});
it('should insert a new image directly into the paragraph that already contains an image', () => {
const mockNode = {
type: { name: 'paragraph' },
content: {
size: 1,
content: [
{
type: { name: 'image', attrs: { src: 'existing-image-url' } },
},
],
},
};
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
mockEditorState.selection.from = 1;
const result = findNodeToInsertImage(mockEditorState, 'image-url');
expect(result.node.type.name).toBe('image');
expect(result.node.attrs.src).toBe('image-url');
expect(result.pos).toBe(1);
});
});
describe('setURLWithQueryAndSize', () => {
let selectedNode;
let editorView;
beforeEach(() => {
selectedNode = {
setAttribute: jest.fn(),
};
const tr = {
setNodeMarkup: jest.fn().mockReturnValue({
docChanged: true,
}),
};
const state = {
selection: { from: 0 },
tr,
};
editorView = {
state,
dispatch: jest.fn(),
};
});
it('updates the URL with the given size and updates the editor view', () => {
const size = { height: '20px' };
setURLWithQueryAndSize(selectedNode, size, editorView);
// Check if the editor view is updated
expect(editorView.dispatch).toHaveBeenCalledTimes(1);
});
it('updates the URL with the given size and updates the editor view with original size', () => {
const size = { height: 'auto' };
setURLWithQueryAndSize(selectedNode, size, editorView);
// Check if the editor view is updated
expect(editorView.dispatch).toHaveBeenCalledTimes(1);
});
it('does not update the editor view if the document has not changed', () => {
editorView.state.tr.setNodeMarkup = jest.fn().mockReturnValue({
docChanged: false,
});
const size = { height: '20px' };
setURLWithQueryAndSize(selectedNode, size, editorView);
// Check if the editor view dispatch was not called
expect(editorView.dispatch).not.toHaveBeenCalled();
});
it('does not perform any operations if selectedNode is not provided', () => {
setURLWithQueryAndSize(null, { height: '20px' }, editorView);
// Ensure the dispatch method wasn't called
expect(editorView.dispatch).not.toHaveBeenCalled();
});
});
@@ -1,100 +0,0 @@
import { findNodeToInsertImage } from '../messageEditorHelper';
describe('findNodeToInsertImage', () => {
let mockEditorState;
beforeEach(() => {
mockEditorState = {
selection: {
$from: {
node: jest.fn(() => ({})),
},
from: 0,
},
schema: {
nodes: {
image: {
create: jest.fn(attrs => ({ type: { name: 'image' }, attrs })),
},
paragraph: {
create: jest.fn((_, node) => ({
type: { name: 'paragraph' },
content: [node],
})),
},
},
},
};
});
it('should insert image directly into an empty paragraph', () => {
const mockNode = {
type: { name: 'paragraph' },
content: { size: 0, content: [] },
};
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
const result = findNodeToInsertImage(mockEditorState, 'image-url');
expect(result).toEqual({
node: { type: { name: 'image' }, attrs: { src: 'image-url' } },
pos: 0,
});
});
it('should insert image directly into a paragraph without an image but with other content', () => {
const mockNode = {
type: { name: 'paragraph' },
content: {
size: 1,
content: [
{
type: { name: 'text' },
},
],
},
};
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
mockEditorState.selection.from = 1;
const result = findNodeToInsertImage(mockEditorState, 'image-url');
expect(result).toEqual({
node: { type: { name: 'image' }, attrs: { src: 'image-url' } },
pos: 2, // Because it should insert after the text, on a new line.
});
});
it("should wrap image in a new paragraph when the current node isn't a paragraph", () => {
const mockNode = {
type: { name: 'not-a-paragraph' },
content: { size: 0, content: [] },
};
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
const result = findNodeToInsertImage(mockEditorState, 'image-url');
expect(result.node.type.name).toBe('paragraph');
expect(result.node.content[0].type.name).toBe('image');
expect(result.node.content[0].attrs.src).toBe('image-url');
expect(result.pos).toBe(0);
});
it('should insert a new image directly into the paragraph that already contains an image', () => {
const mockNode = {
type: { name: 'paragraph' },
content: {
size: 1,
content: [
{
type: { name: 'image', attrs: { src: 'existing-image-url' } },
},
],
},
};
mockEditorState.selection.$from.node.mockReturnValueOnce(mockNode);
mockEditorState.selection.from = 1;
const result = findNodeToInsertImage(mockEditorState, 'image-url');
expect(result.node.type.name).toBe('image');
expect(result.node.attrs.src).toBe('image-url');
expect(result.pos).toBe(1);
});
});
@@ -41,6 +41,16 @@
"STATUS": "Status",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Created at:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send message",
"CANCEL": "Cancel",
"SUCCESS_MESSAGE": "Message sent!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contacts attributes will take precedence.",
"PRIMARY": {
"TITLE": "Primary contact",
"HELP_LABEL": "To be kept"
"HELP_LABEL": "To be deleted"
},
"CHILD": {
"PARENT": {
"TITLE": "Contact to merge",
"PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be deleted"
"HELP_LABEL": "To be kept"
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "No response",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Private Note",
"SEND": "Send",
"CREATE": "Add Note",
"TWEET": "Tweet",
"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",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Copy",
"REPLY_TO": "Reply to this message",
"DELETE": "Delete",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"HELP_CENTER": {
"TITLE": "Help Center (Beta)",
"TITLE": "Help Center",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,6 +41,16 @@
"STATUS": "الحالة",
"ORDER_BY": "الترتيب بواسطة"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "تم إنشاؤها في:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "النشاط الأخير:",
"ACTIVE": "النشاط الأخير"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "النشاط الأخير"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "اكتب رسالتك هنا",
"ERROR": "لا يمكن أن تكون الرسالة فارغة"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "إرسال الرسالة",
"CANCEL": "إلغاء",
"SUCCESS_MESSAGE": "تم إرسال الرسالة!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "دمج جهات الاتصال للجمع بين ملفّين شخصيين في واحد، بما في ذلك جميع السمات والمحادثات. في حالة التعارض، تكون لخصائص جهة الاتصال الرئيسية الأسبقية.",
"PRIMARY": {
"TITLE": "جهة الاتصال الرئيسية",
"HELP_LABEL": "سيتم الاحتفاظ بها"
"HELP_LABEL": "ليتم حذفها"
},
"CHILD": {
"PARENT": {
"TITLE": "دمج جهة الإتصال",
"PLACEHOLDER": "بحث عن جهات الاتصال",
"HELP_LABEL": "ليتم حذفها"
"HELP_LABEL": "سيتم الاحتفاظ بها"
},
"SUMMARY": {
"TITLE": "ملخص",
"DELETE_WARNING": "سيتم حذف جهة الاتصال بـ <strong>%{childContactName}</strong>.",
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال بـ <strong>%{childContactName}</strong> إلى <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "سيتم حذف جهة الاتصال بـ <strong>%{primaryContactName}</strong>.",
"ATTRIBUTE_WARNING": "سيتم نسخ تفاصيل الاتصال بـ <strong>%{primaryContactName}</strong> إلى <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "رسالة_خطأ"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "إسناد لي",
"TWILIO_WHATSAPP_CAN_REPLY": "يمكنك فقط الرد على هذه المحادثة باستخدام رسالة قالب بسبب",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "قيد نافذة الـ 24 ساعة",
"SELECT_A_TWEET_TO_REPLY": "الرجاء تحديد تغريدة للرد عليها.",
"REPLYING_TO": "أنت ترد على:",
"REMOVE_SELECTION": "إزالة التحديد",
"DOWNLOAD": "تنزيل",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "لا توجد استجابة",
"RATING_TITLE": "التقييم",
"FEEDBACK_TITLE": "الملاحظات",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "إظهار السمات",
"HIDE_LABELS": "إخفاء السمات"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "إضافة ملاحظة خاصة",
"SEND": "إرسال",
"CREATE": "إضافة ملاحظة",
"TWEET": "تغريد",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "عرض محرر النصوص",
"TIP_EMOJI_ICON": "إظهار قائمة الرموز التعبيرية",
"TIP_ATTACH_ICON": "إرفاق الملفات",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "نسخ",
"REPLY_TO": "Reply to this message",
"DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "إضافة إلى الردود السريعة",
"TRANSLATE": "ترجم",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "اعرف المزيد",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "نظرة عامة",
"FACEBOOK_REAUTHORIZE": "انتهت صلاحية اتصال الفيسبوك الخاص بك، يرجى إعادة الاتصال بصفحة الفيسبوك الخاصة بك لمواصلة الخدمات",
"HELP_CENTER": {
"TITLE": "مركز المساعدة (نسخة تجريبية)",
"TITLE": "Help Center",
"ALL_ARTICLES": "جميع المقالات",
"MY_ARTICLES": "مقالاتي",
"DRAFT": "مسودة",
@@ -41,6 +41,16 @@
"STATUS": "Статус",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Създаден в:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Напишете съобщението си тук",
"ERROR": "Съобщението не може да е празно"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Изпрати съобщение",
"CANCEL": "Отмени",
"SUCCESS_MESSAGE": "Съобщението е изпратено!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Обединете контактите, за да комбинирате два профила в един, включително всички атрибути и разговори. В случай на конфликт атрибутите на основния контакт ще имат предимство.",
"PRIMARY": {
"TITLE": "Основен контакт",
"HELP_LABEL": "Да бъде запазено"
"HELP_LABEL": "Да бъде изтрит"
},
"CHILD": {
"PARENT": {
"TITLE": "Контакт за обединяване",
"PLACEHOLDER": "Търсене на контакт",
"HELP_LABEL": "Да бъде изтрит"
"HELP_LABEL": "Да бъде запазено"
},
"SUMMARY": {
"TITLE": "Резюме",
"DELETE_WARNING": "Контакт от <strong>%{childContactName}</strong> ще бъде изтрит.",
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>%{childContactName}</strong> ще бъдат копирани в <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "Контакт от <strong>%{primaryContactName}</strong> ще бъде изтрит.",
"ATTRIBUTE_WARNING": "Детайлите на контакт <strong>%{primaryContactName}</strong> ще бъдат копирани в <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ГРЕШКА"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "No response",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Private Note",
"SEND": "Send",
"CREATE": "Add Note",
"TWEET": "Tweet",
"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",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Copy",
"REPLY_TO": "Reply to this message",
"DELETE": "Изтрий",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"HELP_CENTER": {
"TITLE": "Help Center (Beta)",
"TITLE": "Help Center",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,6 +41,16 @@
"STATUS": "Estat",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Created at:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send message",
"CANCEL": "Cancel·la",
"SUCCESS_MESSAGE": "Message sent!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact s attributes will take precedence.",
"PRIMARY": {
"TITLE": "Primary contact",
"HELP_LABEL": "To be kept"
"HELP_LABEL": "To be deleted"
},
"CHILD": {
"PARENT": {
"TITLE": "Contact to merge",
"PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be deleted"
"HELP_LABEL": "To be kept"
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "Restricció de finestra de missatges de 24 hores",
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
"REPLYING_TO": "Estas responent a:",
"REMOVE_SELECTION": "Elimina la selecció",
"DOWNLOAD": "Descarrega",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "Sense resposta",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Nota privada",
"SEND": "Envia",
"CREATE": "Afegeix una nota",
"TWEET": "Tuit",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Mostra l'editor de text enriquit",
"TIP_EMOJI_ICON": "Mostra la selecció d'emoticones",
"TIP_ATTACH_ICON": "Ajuntar fitxers",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Copia",
"REPLY_TO": "Reply to this message",
"DELETE": "Esborrar",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "La teva connexió a Facebook ha caducat, torna a connectar la vostra pàgina de Facebook per continuar els serveis",
"HELP_CENTER": {
"TITLE": "Help Center (Beta)",
"TITLE": "Help Center",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,6 +41,16 @@
"STATUS": "Stav",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Created at:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send message",
"CANCEL": "Zrušit",
"SUCCESS_MESSAGE": "Message sent!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contact s attributes will take precedence.",
"PRIMARY": {
"TITLE": "Primary contact",
"HELP_LABEL": "To be kept"
"HELP_LABEL": "To be deleted"
},
"CHILD": {
"PARENT": {
"TITLE": "Contact to merge",
"PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be deleted"
"HELP_LABEL": "To be kept"
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Přiřadit mi",
"TWILIO_WHATSAPP_CAN_REPLY": "Na tuto konverzaci můžete odpovědět pouze pomocí šablony zprávy z důvodu",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hodinové omezení okna",
"SELECT_A_TWEET_TO_REPLY": "Vyberte prosím tweet, na který chcete odpovědět.",
"REPLYING_TO": "Odpovídáte uživateli:",
"REMOVE_SELECTION": "Odstranit výběr",
"DOWNLOAD": "Stáhnout",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "Bez odpovědi",
"RATING_TITLE": "Hodnocení",
"FEEDBACK_TITLE": "Zpětná vazba",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Soukromá poznámka",
"SEND": "Poslat",
"CREATE": "Přidat poznámku",
"TWEET": "Tweet",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Zobrazit formátovaný textový editor",
"TIP_EMOJI_ICON": "Zobrazit výběr emoji",
"TIP_ATTACH_ICON": "Přiložit soubory",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Kopírovat",
"REPLY_TO": "Reply to this message",
"DELETE": "Vymazat",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"HELP_CENTER": {
"TITLE": "Help Center (Beta)",
"TITLE": "Help Center",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,6 +41,16 @@
"STATUS": "Status",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Oprettet den:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Skriv din besked her",
"ERROR": "Beskeden må ikke være tom"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send besked",
"CANCEL": "Annuller",
"SUCCESS_MESSAGE": "Besked sendt!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Sammenflet kontakter for at kombinere to profiler til én, herunder alle attributter og samtaler. I tilfælde af konflikter vil den primære kontakts attributter have forrang.",
"PRIMARY": {
"TITLE": "Primær kontakt",
"HELP_LABEL": "Skal opbevares"
"HELP_LABEL": "Skal slettes"
},
"CHILD": {
"PARENT": {
"TITLE": "Kontakt for sammenfletning",
"PLACEHOLDER": "Søg efter en kontakt",
"HELP_LABEL": "Skal slettes"
"HELP_LABEL": "Skal opbevares"
},
"SUMMARY": {
"TITLE": "Oversigt",
"DELETE_WARNING": "Kontakt af <strong>%{childContactName}</strong> vil blive slettet.",
"ATTRIBUTE_WARNING": "Kontaktoplysninger på <strong>%{childContactName}</strong> vil blive kopieret til <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "Kontakt af <strong>%{primaryContactName}</strong> vil blive slettet.",
"ATTRIBUTE_WARNING": "Kontaktoplysninger på <strong>%{primaryContactName}</strong> vil blive kopieret til <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "FEJL_MEDDELELSE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Tildel til mig",
"TWILIO_WHATSAPP_CAN_REPLY": "Du kan kun svare på denne samtale ved hjælp af en skabelon besked på grund af",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 timers beskedvindue begrænsning",
"SELECT_A_TWEET_TO_REPLY": "Vælg venligst et tweet at svare på.",
"REPLYING_TO": "Du svarer til:",
"REMOVE_SELECTION": "Fjern Markering",
"DOWNLOAD": "Download",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "Intet svar",
"RATING_TITLE": "Bedømmelse",
"FEEDBACK_TITLE": "Tilbagemelding",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Vis etiketter",
"HIDE_LABELS": "Skjul etiketter"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Privat Note",
"SEND": "Send",
"CREATE": "Tilføj Note",
"TWEET": "Tweet",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Vis rig teksteditor",
"TIP_EMOJI_ICON": "Vis emoji-vælger",
"TIP_ATTACH_ICON": "Vedhæft filer",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Kopiér",
"REPLY_TO": "Reply to this message",
"DELETE": "Slet",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Kan ikke slette kategori"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Lær mere",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Oversigt",
"FACEBOOK_REAUTHORIZE": "Din Facebook-forbindelse er udløbet, tilslut venligst din Facebook-side igen for at fortsætte tjenesterne",
"HELP_CENTER": {
"TITLE": "Hjælpecenter (Beta)",
"TITLE": "Help Center",
"ALL_ARTICLES": "Alle Artikler",
"MY_ARTICLES": "Alle Artikler",
"DRAFT": "Kladde",
@@ -41,6 +41,16 @@
"STATUS": "Status",
"ORDER_BY": "Sortieren nach"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Erstellt",
"OLDEST": "Erstellt am:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Letzte Aktivität:",
"ACTIVE": "Letzte Aktivität"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Letzte Aktivität"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Schreiben Sie Ihre Nachricht hier",
"ERROR": "Nachricht darf nicht leer sein"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Nachricht senden",
"CANCEL": "Abbrechen",
"SUCCESS_MESSAGE": "Nachricht gesendet!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Kontakte zusammenführen, um zwei Profile zu einem zu kombinieren, einschließlich aller Attribute und Gespräche. Im Falle eines Konflikts haben die Attribute des Hauptkontakts Vorrang.",
"PRIMARY": {
"TITLE": "Hauptkontakt",
"HELP_LABEL": "Zu erhalten"
"HELP_LABEL": "Zu löschen"
},
"CHILD": {
"PARENT": {
"TITLE": "Kontakt zum Zusammenführen",
"PLACEHOLDER": "Nach Kontakt suchen",
"HELP_LABEL": "Zu löschen"
"HELP_LABEL": "Zu erhalten"
},
"SUMMARY": {
"TITLE": "Zusammenfassung",
"DELETE_WARNING": "Der Kontakt von <strong>%{childContactName}</strong> wird gelöscht.",
"ATTRIBUTE_WARNING": "Details von Kontakt <strong>%{childContactName}</strong> wird zu <strong>%{primaryContactName}</strong> kopiert."
"DELETE_WARNING": "Der Kontakt von <strong>%{primaryContactName}</strong> wird gelöscht.",
"ATTRIBUTE_WARNING": "Details von Kontakt <strong>%{primaryContactName}</strong> wird zu <strong>%{parentContactName}</strong> kopiert."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Mir zuweisen",
"TWILIO_WHATSAPP_CAN_REPLY": "Sie können auf diese Konversation nur mit einer Nachrichtenvorlage antworten wegen",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24-Stunden-Nachrichtenfenster-Beschränkung",
"SELECT_A_TWEET_TO_REPLY": "Bitte wählen Sie einen Tweet aus, auf welchen Sie antworten möchten.",
"REPLYING_TO": "Sie antworten auf:",
"REMOVE_SELECTION": "Auswahl entfernen",
"DOWNLOAD": "Herunterladen",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "Keine Antwort",
"RATING_TITLE": "Bewertung",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Labels anzeigen",
"HIDE_LABELS": "Labels ausblenden"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Private Notiz",
"SEND": "Senden",
"CREATE": "Notiz hinzufügen",
"TWEET": "Tweet",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Rich Text Editor anzeigen",
"TIP_EMOJI_ICON": "Emoji-Auswahl anzeigen",
"TIP_ATTACH_ICON": "Dateien anhängen",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Kopieren",
"REPLY_TO": "Reply to this message",
"DELETE": "Löschen",
"CREATE_A_CANNED_RESPONSE": "Zu vorgefertigten Antworten hinzufügen",
"TRANSLATE": "Übersetzen",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Kategorie kann nicht gelöscht werden"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Hilfezentrum",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Mehr erfahren",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Übersicht",
"FACEBOOK_REAUTHORIZE": "Ihre Facebook-Verbindung ist abgelaufen, bitte verbinden Sie sich erneut.",
"HELP_CENTER": {
"TITLE": "Hilfezentrum (Beta)",
"TITLE": "Hilfezentrum",
"ALL_ARTICLES": "Alle Artikel",
"MY_ARTICLES": "Meine Artikel",
"DRAFT": "Entwürfe",
@@ -41,6 +41,16 @@
"STATUS": "Κατάσταση",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Δημιουργήθηκε στις:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Γράψτε το μήνυμά σας εδώ",
"ERROR": "Το μήνυμα δεν μπορεί να είναι άδειο"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Αποστολή μηνύματος",
"CANCEL": "Άκυρο",
"SUCCESS_MESSAGE": "Το μήνυμα στάλθηκε!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Συγχώνευση επαφών, για να συνδυάσετε δύο προφίλ σε ένα, συμπεριλαμβανομένων όλων των χαρακτηριστικών και συνομιλιών. Σε περίπτωση σύγκρουσης, τα χαρακτηριστικά της κύριας επαφής υπερισχύουν.",
"PRIMARY": {
"TITLE": "Κύρια επαφή",
"HELP_LABEL": "Να διατηρούνται"
"HELP_LABEL": "Να διαγραφεί"
},
"CHILD": {
"PARENT": {
"TITLE": "Επαφή για συγχώνευση",
"PLACEHOLDER": "Αναζήτηση επαφής",
"HELP_LABEL": "Να διαγραφεί"
"HELP_LABEL": "Να διατηρούνται"
},
"SUMMARY": {
"TITLE": "Σύνοψη",
"DELETE_WARNING": "Η επαφή του <strong>%{childContactName}</strong>θα διαγραφεί.",
"ATTRIBUTE_WARNING": "Τα στοιχεία επικοινωνίας του <strong>%{childContactName}</strong> θα αντιγραφούν στο <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "Η επαφή του <strong>%{primaryContactName}</strong>θα διαγραφεί.",
"ATTRIBUTE_WARNING": "Τα στοιχεία επικοινωνίας του <strong>%{primaryContactName}</strong> θα αντιγραφούν στο <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Ανάθεση σε μένα",
"TWILIO_WHATSAPP_CAN_REPLY": "Μπορείτε να απαντήσετε μόνο σε αυτή τη συνομιλία χρησιμοποιώντας ένα πρότυπο μήνυμα επειδή",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "του περιορισμού των 24 ωρών",
"SELECT_A_TWEET_TO_REPLY": "Παρακαλώ επιλέξτε ένα tweet για να απαντήσετε.",
"REPLYING_TO": "Απαντάτε στο:",
"REMOVE_SELECTION": "Διαγραφή Επιλογής",
"DOWNLOAD": "Κατέβασμα",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "Καμία ανταπόκριση",
"RATING_TITLE": "Αξιολόγηση",
"FEEDBACK_TITLE": "Ανατροφοδότηση",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Προβολή ετικετών",
"HIDE_LABELS": "Απόκρυψη ετικετών"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Ιδιωτική Σημείωση",
"SEND": "Αποστολή",
"CREATE": "Σημείωση (Add Note)",
"TWEET": "Tweet",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Προβολή επεξεργαστή εμπλουτισμένου κειμένου",
"TIP_EMOJI_ICON": "Προβολή επιλογέα emoji",
"TIP_ATTACH_ICON": "Επισύναψη αρχείων",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Αντιγραφή",
"REPLY_TO": "Reply to this message",
"DELETE": "Διαγραφή",
"CREATE_A_CANNED_RESPONSE": "Προσθήκη στις έτοιμες απαντήσεις",
"TRANSLATE": "Translate",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Δεν είναι δυνατή η διαγραφή κατηγορίας"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Μάθετε περισσότερα",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Επισκόπηση",
"FACEBOOK_REAUTHORIZE": "Η σύνδεση Facebook έχει λήξει, παρακαλώ ξανασυνδεθείτε στο Facebook για να συνεχίσετε",
"HELP_CENTER": {
"TITLE": "Κέντρο Βοήθειας (Beta)",
"TITLE": "Help Center",
"ALL_ARTICLES": "Όλα Τα Άρθρα",
"MY_ARTICLES": "Τα Άρθρα Μου",
"DRAFT": "Πρόχειρο",
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Send message",
"CANCEL": "Cancel",
"SUCCESS_MESSAGE": "Message sent!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Merge contacts to combine two profiles into one, including all attributes and conversations. In case of conflict, the Primary contacts attributes will take precedence.",
"PRIMARY": {
"TITLE": "Primary contact",
"HELP_LABEL": "To be kept"
"HELP_LABEL": "To be deleted"
},
"CHILD": {
"PARENT": {
"TITLE": "Contact to merge",
"PLACEHOLDER": "Search for a contact",
"HELP_LABEL": "To be deleted"
"HELP_LABEL": "To be kept"
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Assign to me",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24 hour message window restriction",
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
"REPLYING_TO": "You are replying to:",
"REMOVE_SELECTION": "Remove Selection",
"DOWNLOAD": "Download",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "No response",
"RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Private Note",
"SEND": "Send",
"CREATE": "Add Note",
"TWEET": "Tweet",
"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",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Copy",
"REPLY_TO": "Reply to this message",
"DELETE": "Delete",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -238,7 +238,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Your Facebook connection has expired, please reconnect your Facebook page to continue services",
"HELP_CENTER": {
"TITLE": "Help Center (Beta)",
"TITLE": "Help Center",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",
@@ -41,6 +41,16 @@
"STATUS": "Estado",
"ORDER_BY": "Ordenar por"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Creado",
"OLDEST": "Creado el:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Última actividad:",
"ACTIVE": "Última actividad"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Última actividad"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Escriba su mensaje aquí",
"ERROR": "El mensaje no puede estar vacío"
},
"ATTACHMENTS": {
"SELECT": "Seleccionar archivos",
"HELP_TEXT": "Arrastre y suelte los archivos aquí o elige archivos para adjuntar"
},
"SUBMIT": "Enviar mensaje",
"CANCEL": "Cancelar",
"SUCCESS_MESSAGE": "¡Mensaje enviado!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Fusionar los contactos para combinar dos perfiles en uno, incluyendo todos los atributos y conversaciones.",
"PRIMARY": {
"TITLE": "Contacto principal",
"HELP_LABEL": "Mantener"
"HELP_LABEL": "A eliminar"
},
"CHILD": {
"PARENT": {
"TITLE": "Contacto para combinar",
"PLACEHOLDER": "Buscar un contacto",
"HELP_LABEL": "A eliminar"
"HELP_LABEL": "Mantener"
},
"SUMMARY": {
"TITLE": "Resumen",
"DELETE_WARNING": "El contacto de <strong>%{childContactName}</strong> se eliminará.",
"ATTRIBUTE_WARNING": "Los datos de contacto de <strong>%{childContactName}</strong> se copiarán en <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "El contacto de <strong>%{primaryContactName}</strong> se eliminará.",
"ATTRIBUTE_WARNING": "Los datos de contacto de <strong>%{primaryContactName}</strong> se copiarán en <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Asignar a mi",
"TWILIO_WHATSAPP_CAN_REPLY": "Sólo puede responder a esta conversación usando una plantilla de mensaje debido a",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "Restricción de la ventana de mensajes de 24 horas",
"SELECT_A_TWEET_TO_REPLY": "Por favor, seleccione un tweet al que responder.",
"REPLYING_TO": "Esta respondiendo a:",
"REMOVE_SELECTION": "Eliminar selección",
"DOWNLOAD": "Descargar",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "No hay respuesta",
"RATING_TITLE": "Calificación",
"FEEDBACK_TITLE": "Comentarios",
"REPLY_MESSAGE_NOT_FOUND": "Mensaje no disponible",
"CARD": {
"SHOW_LABELS": "Mostrar etiquetas",
"HIDE_LABELS": "Ocultar etiquetas"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Nota privada",
"SEND": "Enviar",
"CREATE": "Añadir nota",
"TWEET": "Tweet",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "Mostrar editor de textos",
"TIP_EMOJI_ICON": "Mostrar selector de emoji",
"TIP_ATTACH_ICON": "Adjuntar archivos",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Copiar",
"REPLY_TO": "Reply to this message",
"DELETE": "Eliminar",
"CREATE_A_CANNED_RESPONSE": "Añadir a respuestas predefinidas",
"TRANSLATE": "Traducir",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "No se pudo borrar la categoría"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Centro de ayuda",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Más información",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Resumen",
"FACEBOOK_REAUTHORIZE": "Su conexión de Facebook expiró, por favor reconecte si página de Facebook para continuar con el servicio",
"HELP_CENTER": {
"TITLE": "Centro de ayuda (Beta)",
"TITLE": "Centro de ayuda",
"ALL_ARTICLES": "Todos los artículos",
"MY_ARTICLES": "Todos los artículos",
"DRAFT": "Borrador",
@@ -41,6 +41,16 @@
"STATUS": "وضعیت",
"ORDER_BY": "سفارش توسط"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "ایجاد شده",
"OLDEST": "ایجاد شده در:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "آخرین فعالیت:",
"ACTIVE": "آخرین فعالیت"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "آخرین فعالیت"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "پیام خود را اینجا بنویسید",
"ERROR": "پیام نمی‌تواند خالی باشد"
},
"ATTACHMENTS": {
"SELECT": "پرونده‌ها را انتخاب کنید",
"HELP_TEXT": "پرونده‌ها را اینجا بکشید و رها کنید یا پرونده‌ها را برای پیوست انتخاب کنید"
},
"SUBMIT": "ارسال پیام",
"CANCEL": "انصراف",
"SUCCESS_MESSAGE": "پیام ارسال شد!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "با ادغام مخاطبین دو پروفایل به همراه تمام ویژگی ها و گفتگوها با هم ادغام می شود. در صورت تعارض، ویژگی های تماس اولیه اولویت دارد.",
"PRIMARY": {
"TITLE": "مخاطب اصلی",
"HELP_LABEL": "نگه داشتن"
"HELP_LABEL": "حذف شود"
},
"CHILD": {
"PARENT": {
"TITLE": "ادغام مخاطب",
"PLACEHOLDER": "جستجو برای یک مخاطب",
"HELP_LABEL": "حذف شود"
"HELP_LABEL": "نگه داشتن"
},
"SUMMARY": {
"TITLE": "خلاصه",
"DELETE_WARNING": "اطلاعات مخاطب <strong>%{childContactName}</strong> حذف خواهد شد.",
"ATTRIBUTE_WARNING": "اطلاعات مخاطب <strong>%{childContactName}</strong> کپی شود به <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "اطلاعات مخاطب <strong>%{primaryContactName}</strong> حذف خواهد شد.",
"ATTRIBUTE_WARNING": "اطلاعات مخاطب <strong>%{primaryContactName}</strong> کپی شود به <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "اختصاص به من",
"TWILIO_WHATSAPP_CAN_REPLY": "شما فقط می توانید با استفاده از یک پیام الگو به این مکالمه پاسخ دهید",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "محدودیت ۲۴ ساعته پنجره پیام",
"SELECT_A_TWEET_TO_REPLY": "لطفا برای پاسخ دادن، یک توییت را انتخاب کنید.",
"REPLYING_TO": "شما در حال پاسخ دادن به:",
"REMOVE_SELECTION": "حذف انتخاب‌شده‌ها",
"DOWNLOAD": "دانلود",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "بدون پاسخ",
"RATING_TITLE": "رتبه",
"FEEDBACK_TITLE": "بازخورد",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "مشاهده کردن برچسب‌ها",
"HIDE_LABELS": "پنهان کردن برچسب‌ها"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "یادداشت خصوصی",
"SEND": "ارسال",
"CREATE": "افزودن یادداشت",
"TWEET": "توییت",
"DISMISS_REPLY": "Dismiss reply",
"REPLYING_TO": "Replying to:",
"TIP_FORMAT_ICON": "نمایش ویرایشگر متنی پیشرفته",
"TIP_EMOJI_ICON": "انتخاب ایموجی",
"TIP_ATTACH_ICON": "ضمیمه فایل",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "کپی",
"REPLY_TO": "Reply to this message",
"DELETE": "حذف",
"CREATE_A_CANNED_RESPONSE": "اضافه کردن به پاسخ‌های آماده",
"TRANSLATE": "ترجمه",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "حذف دسته‌بندی ممکن نیست"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "مرکز راهنما",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "بیشتر بدانید",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "بررسی اجمالی",
"FACEBOOK_REAUTHORIZE": "اتصال فیس بوک شما منقضی شده است ، لطفاً برای ادامه خدمات دوباره صفحه فیس بوک خود را متصل کنید",
"HELP_CENTER": {
"TITLE": "مرکز راهنمایی (آزمایشی)",
"TITLE": "مرکز راهنما",
"ALL_ARTICLES": "همه مقالات",
"MY_ARTICLES": "مقالات من",
"DRAFT": "پیش‌نویس",
@@ -41,6 +41,16 @@
"STATUS": "Tila",
"ORDER_BY": "Order by"
},
"CHAT_TIME_STAMP": {
"CREATED": {
"LATEST": "Created",
"OLDEST": "Created at:"
},
"LAST_ACTIVITY": {
"NOT_ACTIVE": "Last activity:",
"ACTIVE": "Last activity"
}
},
"CHAT_SORT_FILTER_ITEMS": {
"latest": {
"TEXT": "Last activity"
@@ -204,6 +204,10 @@
"PLACEHOLDER": "Write your message here",
"ERROR": "Message can't be empty"
},
"ATTACHMENTS": {
"SELECT": "Choose files",
"HELP_TEXT": "Drag and drop files here or choose files to attach"
},
"SUBMIT": "Lähetä viesti",
"CANCEL": "Peruuta",
"SUCCESS_MESSAGE": "Message sent!",
@@ -343,17 +347,17 @@
"DESCRIPTION": "Yhdistämällä yhteystiedot voit muuntaa kaksi profiilia yhdeksi, mukaan lukien kaikki määritteet ja keskustelut. Ristiriitatilanteessa ensisijaisen yhteyshenkilön määritteet ovat ensisijaisia.",
"PRIMARY": {
"TITLE": "Ensisijainen kontakti",
"HELP_LABEL": "Säilytetään"
"HELP_LABEL": "Poistetaan"
},
"CHILD": {
"PARENT": {
"TITLE": "Yhdistettävä yhteystieto",
"PLACEHOLDER": "Etsi yhteystietoa",
"HELP_LABEL": "Poistetaan"
"HELP_LABEL": "Säilytetään"
},
"SUMMARY": {
"TITLE": "Summary",
"DELETE_WARNING": "Contact of <strong>%{childContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{childContactName}</strong> will be copied to <strong>%{primaryContactName}</strong>."
"DELETE_WARNING": "Contact of <strong>%{primaryContactName}</strong> will be deleted.",
"ATTRIBUTE_WARNING": "Contact details of <strong>%{primaryContactName}</strong> will be copied to <strong>%{parentContactName}</strong>."
},
"SEARCH": {
"ERROR": "ERROR_MESSAGE"
@@ -34,7 +34,6 @@
"ASSIGN_TO_ME": "Siirrä minulle",
"TWILIO_WHATSAPP_CAN_REPLY": "You can only reply to this conversation using a template message due to",
"TWILIO_WHATSAPP_24_HOURS_WINDOW": "24h vastausikkuna",
"SELECT_A_TWEET_TO_REPLY": "Please select a tweet to reply to.",
"REPLYING_TO": "Olet vastaamassa:",
"REMOVE_SELECTION": "Poista valinnat",
"DOWNLOAD": "Lataa",
@@ -47,6 +46,7 @@
"NO_RESPONSE": "No response",
"RATING_TITLE": "Arvio",
"FEEDBACK_TITLE": "Palaute",
"REPLY_MESSAGE_NOT_FOUND": "Message not available",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
@@ -139,7 +139,8 @@
"PRIVATE_NOTE": "Sisäinen merkintä",
"SEND": "Lähetä",
"CREATE": "Lisää muistiinpano",
"TWEET": "Twiittaa",
"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",
@@ -194,6 +195,7 @@
},
"CONTEXT_MENU": {
"COPY": "Kopioi",
"REPLY_TO": "Reply to this message",
"DELETE": "Poista",
"CREATE_A_CANNED_RESPONSE": "Add to canned responses",
"TRANSLATE": "Translate",
@@ -425,6 +425,33 @@
"ERROR_MESSAGE": "Unable to delete category"
}
}
},
"UPGRADE_PAGE": {
"TITLE": "Help Center",
"DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Upgrade your subscription to enable this feature.",
"SELF_HOSTED_DESCRIPTION": "Create user-friendly self-service portals. Help your users to access the articles and get support 24/7. Please contact your administrator to enable this feature.",
"BUTTON": {
"LEARN_MORE": "Learn more",
"UPGRADE": "Upgrade"
},
"FEATURES": {
"PORTALS": {
"TITLE": "Multiple portals",
"DESCRIPTION": "Create multiple help center portals for different products using the same account."
},
"LOCALES": {
"TITLE": "Full support for locales",
"DESCRIPTION": "Localize the portal in your language. We support all locales and allow translations for every article."
},
"SEO": {
"TITLE": "SEO-friendly design",
"DESCRIPTION": "Customize your meta tags to improve your visibility on search engines with our SEO-friendly pages."
},
"API": {
"TITLE": "Full API support",
"DESCRIPTION": "Use the portal as a headless CMS with third party front-end frameworks using our APIs."
}
}
}
}
}
@@ -242,7 +242,7 @@
"REPORTS_OVERVIEW": "Overview",
"FACEBOOK_REAUTHORIZE": "Facebook-yhteytesi on vanhentunut, ole hyvä ja yhdistä uudelleen Facebook-sivusi jatkaaksesi palveluita",
"HELP_CENTER": {
"TITLE": "Help Center (Beta)",
"TITLE": "Help Center",
"ALL_ARTICLES": "All Articles",
"MY_ARTICLES": "My Articles",
"DRAFT": "Draft",

Some files were not shown because too many files have changed in this diff Show More