mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-02 03:57:52 +00:00
Merge branch 'release/1.4.0'
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
version: "2"
|
||||
plugins:
|
||||
rubocop:
|
||||
enabled: true
|
||||
enabled: false
|
||||
channel: rubocop-0-73
|
||||
eslint:
|
||||
enabled: false
|
||||
|
||||
57
.env.example
57
.env.example
@@ -1,14 +1,19 @@
|
||||
SECRET_KEY_BASE=
|
||||
# Used to verify the integrity of signed cookies. so ensure a secure value is set
|
||||
SECRET_KEY_BASE=replace_with_lengthy_secure_hex
|
||||
|
||||
# Replace with the URL you are planning to use for your app
|
||||
FRONTEND_URL=http://0.0.0.0:3000
|
||||
|
||||
# Force all access to the app over SSL, default is set to false
|
||||
FORCE_SSL=
|
||||
FORCE_SSL=false
|
||||
|
||||
# This lets you control new sign ups on your chatwoot installation
|
||||
# true : default option, allows sign ups
|
||||
# false : disables all the end points related to sign ups
|
||||
# api_only: disables the UI for signup, but you can create sign ups via the account apis
|
||||
ENABLE_ACCOUNT_SIGNUP=
|
||||
ENABLE_ACCOUNT_SIGNUP=true
|
||||
|
||||
#redis config
|
||||
# Redis config
|
||||
REDIS_URL=redis://redis:6379
|
||||
# If you are using docker-compose, set this variable's value to be any string,
|
||||
# which will be the password for the redis service running inside the docker-compose
|
||||
@@ -22,7 +27,7 @@ POSTGRES_PASSWORD=
|
||||
RAILS_ENV=development
|
||||
RAILS_MAX_THREADS=5
|
||||
|
||||
#mail
|
||||
# Mail outgoing
|
||||
MAILER_SENDER_EMAIL=accounts@chatwoot.com
|
||||
SMTP_PORT=1025
|
||||
SMTP_DOMAIN=chatwoot.com
|
||||
@@ -34,39 +39,60 @@ SMTP_PASSWORD=
|
||||
SMTP_AUTHENTICATION=
|
||||
SMTP_ENABLE_STARTTLS_AUTO=
|
||||
|
||||
#misc
|
||||
FRONTEND_URL=http://0.0.0.0:3000
|
||||
# Mail Incoming
|
||||
|
||||
# Set this to appropriate ingress channel with regards to incoming emails
|
||||
# Possible values are :
|
||||
# :relay for Exim, Postfix, Qmail
|
||||
# :mailgun for Mailgun
|
||||
# :mandrill for Mandrill
|
||||
# :postmark for Postmark
|
||||
# :sendgrid for Sendgrid
|
||||
RAILS_INBOUND_EMAIL_SERVICE=
|
||||
# Use one of the following based on the email ingress service
|
||||
# Ref: https://edgeguides.rubyonrails.org/action_mailbox_basics.html
|
||||
RAILS_INBOUND_EMAIL_PASSWORD=
|
||||
MAILGUN_INGRESS_SIGNING_KEY=
|
||||
MANDRILL_INGRESS_API_KEY=
|
||||
|
||||
# Storage
|
||||
ACTIVE_STORAGE_SERVICE=local
|
||||
|
||||
#s3
|
||||
# Amazon S3
|
||||
S3_BUCKET_NAME=
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_REGION=
|
||||
|
||||
#sentry
|
||||
# Sentry
|
||||
SENTRY_DSN=
|
||||
|
||||
#Log settings
|
||||
# Log settings
|
||||
# Disable if you want to write logs to a file
|
||||
RAILS_LOG_TO_STDOUT=true
|
||||
LOG_LEVEL=info
|
||||
LOG_SIZE=500
|
||||
LOG_SIZE=500
|
||||
|
||||
# Credentials to access sidekiq dashboard in production
|
||||
SIDEKIQ_AUTH_USERNAME=
|
||||
SIDEKIQ_AUTH_PASSWORD=
|
||||
|
||||
### This environment variables are only required if you are setting up social media channels
|
||||
#facebook
|
||||
#facebook
|
||||
FB_VERIFY_TOKEN=
|
||||
FB_APP_SECRET=
|
||||
FB_APP_ID=
|
||||
|
||||
#twitter
|
||||
# Twitter
|
||||
TWITTER_APP_ID=
|
||||
TWITTER_CONSUMER_KEY=
|
||||
TWITTER_CONSUMER_SECRET=
|
||||
TWITTER_ENVIRONMENT=
|
||||
|
||||
### Change this env variable only if you are using a custom build mobile app
|
||||
## Mobile app env variables
|
||||
IOS_APP_ID=6C953F3RX2.com.chatwoot.app
|
||||
|
||||
#### This environment variables are only required in hosted version which has billing
|
||||
ENABLE_BILLING=
|
||||
|
||||
@@ -75,3 +101,8 @@ CHARGEBEE_API_KEY=
|
||||
CHARGEBEE_SITE=
|
||||
CHARGEBEE_WEBHOOK_USERNAME=
|
||||
CHARGEBEE_WEBHOOK_PASSWORD=
|
||||
|
||||
## Push Notification
|
||||
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
||||
# VAPID_PUBLIC_KEY=
|
||||
# VAPID_PRIVATE_KEY=
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module.exports = {
|
||||
extends: ['airbnb/base', 'prettier', 'plugin:vue/recommended'],
|
||||
extends: ['airbnb-base/legacy', 'prettier', 'plugin:vue/recommended'],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint',
|
||||
ecmaVersion: 2017,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['html', 'prettier', 'babel'],
|
||||
|
||||
12
.rubocop.yml
12
.rubocop.yml
@@ -4,18 +4,28 @@ require:
|
||||
- rubocop-rspec
|
||||
inherit_from: .rubocop_todo.yml
|
||||
|
||||
Lint/RaiseException:
|
||||
Enabled: true
|
||||
Lint/StructNewOverride:
|
||||
Enabled: true
|
||||
Layout/LineLength:
|
||||
Max: 150
|
||||
Metrics/ClassLength:
|
||||
Max: 125
|
||||
RSpec/ExampleLength:
|
||||
Max: 15
|
||||
Max: 25
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
Style/SymbolArray:
|
||||
Enabled: false
|
||||
Style/HashEachMethods:
|
||||
Enabled: true
|
||||
Style/HashTransformKeys:
|
||||
Enabled: true
|
||||
Style/HashTransformValues:
|
||||
Enabled: true
|
||||
Style/GlobalVars:
|
||||
Exclude:
|
||||
- 'config/initializers/redis.rb'
|
||||
|
||||
@@ -252,7 +252,7 @@ linters:
|
||||
enabled: false
|
||||
|
||||
UnnecessaryParentReference:
|
||||
enabled: true
|
||||
enabled: false
|
||||
|
||||
UrlFormat:
|
||||
enabled: true
|
||||
|
||||
9
Gemfile
9
Gemfile
@@ -17,6 +17,7 @@ gem 'jbuilder'
|
||||
gem 'kaminari'
|
||||
gem 'responders'
|
||||
gem 'rest-client'
|
||||
gem 'telephone_number'
|
||||
gem 'time_diff'
|
||||
gem 'tzinfo-data'
|
||||
gem 'valid_email2'
|
||||
@@ -53,9 +54,6 @@ gem 'pundit'
|
||||
# https://karolgalanciak.com/blog/2019/11/30/from-activerecord-callbacks-to-publish-slash-subscribe-pattern-and-event-driven-design/
|
||||
gem 'wisper', '2.0.0'
|
||||
|
||||
##--- gems for reporting ---##
|
||||
gem 'nightfury'
|
||||
|
||||
##--- gems for billing ---##
|
||||
gem 'chargebee'
|
||||
|
||||
@@ -82,6 +80,9 @@ gem 'sidekiq'
|
||||
##-- used for single column multiple binary flags in notification settings/feature flagging --##
|
||||
gem 'flag_shih_tzu'
|
||||
|
||||
##-- Push notification service --##
|
||||
gem 'webpush'
|
||||
|
||||
group :development do
|
||||
gem 'annotate'
|
||||
gem 'bullet'
|
||||
@@ -100,7 +101,7 @@ group :development, :test do
|
||||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'listen'
|
||||
gem 'mock_redis'
|
||||
gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9'
|
||||
gem 'pry-rails'
|
||||
gem 'rspec-rails', '~> 4.0.0.beta2'
|
||||
gem 'rubocop', require: false
|
||||
|
||||
51
Gemfile.lock
51
Gemfile.lock
@@ -5,6 +5,13 @@ GIT
|
||||
twitty (0.1.0)
|
||||
oauth
|
||||
|
||||
GIT
|
||||
remote: https://github.com/sds/mock_redis
|
||||
revision: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
ref: 16d00789f0341a3aac35126c0ffe97a596753ff9
|
||||
specs:
|
||||
mock_redis (0.22.0)
|
||||
|
||||
GIT
|
||||
remote: https://github.com/tzmfreedom/json_refs
|
||||
revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d
|
||||
@@ -82,10 +89,10 @@ GEM
|
||||
rake (>= 10.4, < 14.0)
|
||||
ast (2.4.0)
|
||||
attr_extras (6.2.3)
|
||||
aws-eventstream (1.0.3)
|
||||
aws-partitions (1.294.0)
|
||||
aws-sdk-core (3.92.0)
|
||||
aws-eventstream (~> 1.0, >= 1.0.2)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.296.0)
|
||||
aws-sdk-core (3.94.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
jmespath (~> 1.0)
|
||||
@@ -113,7 +120,7 @@ GEM
|
||||
bindex (0.8.1)
|
||||
bootsnap (1.4.6)
|
||||
msgpack (~> 1.0)
|
||||
brakeman (4.8.0)
|
||||
brakeman (4.8.1)
|
||||
browser (4.0.0)
|
||||
builder (3.2.4)
|
||||
bullet (6.1.0)
|
||||
@@ -179,7 +186,7 @@ GEM
|
||||
foreman (0.87.1)
|
||||
globalid (0.4.2)
|
||||
activesupport (>= 4.2.0)
|
||||
google-api-client (0.37.2)
|
||||
google-api-client (0.38.0)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (~> 0.9)
|
||||
httpclient (>= 2.8.1, < 3.0)
|
||||
@@ -193,25 +200,26 @@ GEM
|
||||
google-cloud-env (1.3.1)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
google-cloud-errors (1.0.0)
|
||||
google-cloud-storage (1.25.1)
|
||||
google-cloud-storage (1.26.0)
|
||||
addressable (~> 2.5)
|
||||
digest-crc (~> 0.4)
|
||||
google-api-client (~> 0.33)
|
||||
google-cloud-core (~> 1.2)
|
||||
googleauth (~> 0.9)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (0.11.0)
|
||||
googleauth (0.12.0)
|
||||
faraday (>= 0.17.3, < 2.0)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
memoist (~> 0.16)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (~> 0.12)
|
||||
signet (~> 0.14)
|
||||
groupdate (5.0.0)
|
||||
activesupport (>= 5)
|
||||
haikunator (1.1.0)
|
||||
hana (1.3.5)
|
||||
hashie (4.1.0)
|
||||
hkdf (0.3.0)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.3)
|
||||
domain_name (~> 0.5)
|
||||
@@ -253,7 +261,7 @@ GEM
|
||||
listen (3.2.1)
|
||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||
rb-inotify (~> 0.9, >= 0.9.10)
|
||||
loofah (2.4.0)
|
||||
loofah (2.5.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
mail (2.7.1)
|
||||
@@ -270,13 +278,11 @@ GEM
|
||||
mini_mime (1.0.2)
|
||||
mini_portile2 (2.4.0)
|
||||
minitest (5.14.0)
|
||||
mock_redis (0.22.0)
|
||||
msgpack (1.3.3)
|
||||
multi_json (1.14.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.1.1)
|
||||
netrc (0.11.0)
|
||||
nightfury (1.0.1)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.9)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
@@ -284,21 +290,21 @@ GEM
|
||||
orm_adapter (0.5.0)
|
||||
os (1.1.0)
|
||||
parallel (1.19.1)
|
||||
parser (2.7.1.0)
|
||||
parser (2.7.1.1)
|
||||
ast (~> 2.4.0)
|
||||
pg (1.2.3)
|
||||
pry (0.13.0)
|
||||
pry (0.13.1)
|
||||
coderay (~> 1.1)
|
||||
method_source (~> 1.0)
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (4.0.3)
|
||||
public_suffix (4.0.4)
|
||||
puma (4.3.3)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.1.0)
|
||||
activesupport (>= 3.0.0)
|
||||
rack (2.2.2)
|
||||
rack-cache (1.11.0)
|
||||
rack-cache (1.11.1)
|
||||
rack (>= 0.4)
|
||||
rack-cors (1.1.1)
|
||||
rack (>= 2.0.0)
|
||||
@@ -388,7 +394,7 @@ GEM
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-performance (1.5.2)
|
||||
rubocop (>= 0.71.0)
|
||||
rubocop-rails (2.5.1)
|
||||
rubocop-rails (2.5.2)
|
||||
activesupport
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.72.0)
|
||||
@@ -442,6 +448,7 @@ GEM
|
||||
faraday
|
||||
inflecto
|
||||
virtus
|
||||
telephone_number (1.4.6)
|
||||
thor (0.20.3)
|
||||
thread_safe (0.3.6)
|
||||
time_diff (0.3.0)
|
||||
@@ -463,7 +470,7 @@ GEM
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
uniform_notifier (1.13.0)
|
||||
valid_email2 (3.2.1)
|
||||
valid_email2 (3.2.2)
|
||||
activemodel (>= 3.2)
|
||||
mail (~> 2.5)
|
||||
virtus (1.0.5)
|
||||
@@ -483,6 +490,9 @@ GEM
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
semantic_range (>= 2.3.0)
|
||||
webpush (1.0.0)
|
||||
hkdf (~> 0.2)
|
||||
jwt (~> 2.0)
|
||||
websocket-driver (0.7.1)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.4)
|
||||
@@ -526,8 +536,7 @@ DEPENDENCIES
|
||||
letter_opener
|
||||
listen
|
||||
mini_magick
|
||||
mock_redis
|
||||
nightfury
|
||||
mock_redis!
|
||||
pg
|
||||
pry-rails
|
||||
puma
|
||||
@@ -554,6 +563,7 @@ DEPENDENCIES
|
||||
spring
|
||||
spring-watcher-listen
|
||||
telegram-bot-ruby
|
||||
telephone_number
|
||||
time_diff
|
||||
twilio-ruby (~> 5.32.0)
|
||||
twitty!
|
||||
@@ -562,6 +572,7 @@ DEPENDENCIES
|
||||
valid_email2
|
||||
web-console
|
||||
webpacker
|
||||
webpush
|
||||
wisper (= 2.0.0)
|
||||
|
||||
RUBY VERSION
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p align="center">
|
||||
<img src="https://storage.googleapis.com/chatwoot-assets/woot-logo.svg" alt="Woot-logo" width="240">
|
||||
<img src="https://s3.us-west-2.amazonaws.com/gh-assets.chatwoot.com/brand.svg" alt="Woot-logo" width="240">
|
||||
|
||||
<div align="center">A simple and elegant live chat software</div>
|
||||
<div align="center">An opensource alternative to Intercom, Zendesk, Drift, Crisp etc.</div>
|
||||
@@ -23,7 +23,7 @@ ___
|
||||
<a href="https://discord.gg/cJXdrwS"><img src="https://img.shields.io/badge/chat-Discord-violet?logo=discord" alt="Chat on Discord"></a>
|
||||
</p>
|
||||
|
||||

|
||||

|
||||
|
||||
## Background
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class ContactMergeAction
|
||||
def validate_contacts
|
||||
return if belongs_to_account?(@base_contact) && belongs_to_account?(@mergee_contact)
|
||||
|
||||
raise Exception, 'contact does not belong to the account'
|
||||
raise StandardError, 'contact does not belong to the account'
|
||||
end
|
||||
|
||||
def belongs_to_account?(contact)
|
||||
|
||||
@@ -21,13 +21,14 @@ class ContactBuilder
|
||||
phone_number: contact_attributes[:phone_number],
|
||||
email: contact_attributes[:email],
|
||||
identifier: contact_attributes[:identifier],
|
||||
additional_attributes: contact_attributes[:identifier]
|
||||
additional_attributes: contact_attributes[:additional_attributes]
|
||||
)
|
||||
contact_inbox = ::ContactInbox.create!(
|
||||
contact_id: contact.id,
|
||||
inbox_id: inbox.id,
|
||||
source_id: source_id
|
||||
)
|
||||
|
||||
::ContactAvatarJob.perform_later(contact, contact_attributes[:avatar_url]) if contact_attributes[:avatar_url]
|
||||
contact_inbox
|
||||
rescue StandardError => e
|
||||
|
||||
@@ -41,7 +41,7 @@ class Messages::MessageBuilder
|
||||
def build_message
|
||||
@message = conversation.messages.create!(message_params)
|
||||
(response.attachments || []).each do |attachment|
|
||||
attachment_obj = @message.build_attachment(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url))
|
||||
attachment_obj.save!
|
||||
attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url]
|
||||
end
|
||||
|
||||
@@ -3,22 +3,26 @@ class Messages::Outgoing::NormalBuilder
|
||||
attr_reader :message
|
||||
|
||||
def initialize(user, conversation, params)
|
||||
@content = params[:message]
|
||||
@content = params[:content]
|
||||
@private = params[:private] || false
|
||||
@conversation = conversation
|
||||
@user = user
|
||||
@fb_id = params[:fb_id]
|
||||
@attachment = params[:attachment]
|
||||
@content_type = params[:content_type]
|
||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
||||
@attachments = params[:attachments]
|
||||
end
|
||||
|
||||
def perform
|
||||
@message = @conversation.messages.build(message_params)
|
||||
if @attachment
|
||||
@message.attachment = Attachment.new(
|
||||
account_id: message.account_id,
|
||||
file_type: file_type(@attachment[:file]&.content_type)
|
||||
)
|
||||
@message.attachment.file.attach(@attachment[:file])
|
||||
if @attachments.present?
|
||||
@attachments.each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: file_type(uploaded_attachment&.content_type)
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
end
|
||||
@message.save
|
||||
@message
|
||||
@@ -34,7 +38,9 @@ class Messages::Outgoing::NormalBuilder
|
||||
content: @content,
|
||||
private: @private,
|
||||
user_id: @user&.id,
|
||||
source_id: @fb_id
|
||||
source_id: @fb_id,
|
||||
content_type: @content_type,
|
||||
items: @items
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
32
app/builders/notification_builder.rb
Normal file
32
app/builders/notification_builder.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
class NotificationBuilder
|
||||
pattr_initialize [:notification_type!, :user!, :account!, :primary_actor!]
|
||||
|
||||
def perform
|
||||
return unless user_subscribed_to_notification?
|
||||
|
||||
build_notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def secondary_actor
|
||||
Current.user
|
||||
end
|
||||
|
||||
def user_subscribed_to_notification?
|
||||
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
||||
return true if notification_setting.public_send("email_#{notification_type}?")
|
||||
return true if notification_setting.public_send("push_#{notification_type}?")
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def build_notification
|
||||
user.notifications.create!(
|
||||
notification_type: notification_type,
|
||||
account: account,
|
||||
primary_actor: primary_actor,
|
||||
secondary_actor: secondary_actor
|
||||
)
|
||||
end
|
||||
end
|
||||
28
app/builders/notification_subscription_builder.rb
Normal file
28
app/builders/notification_subscription_builder.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
class NotificationSubscriptionBuilder
|
||||
pattr_initialize [:params, :user!]
|
||||
|
||||
def perform
|
||||
# if multiple accounts were used to login in same browser
|
||||
move_subscription_to_user if identifier_subscription && identifier_subscription.user_id != user.id
|
||||
build_identifier_subscription if identifier_subscription.blank?
|
||||
identifier_subscription
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def identifier
|
||||
@identifier ||= params[:subscription_attributes][:endpoint] if params[:subscription_type] == 'browser_push'
|
||||
end
|
||||
|
||||
def identifier_subscription
|
||||
@identifier_subscription ||= NotificationSubscription.find_by(identifier: identifier)
|
||||
end
|
||||
|
||||
def move_subscription_to_user
|
||||
@identifier_subscription.update(user_id: user.id)
|
||||
end
|
||||
|
||||
def build_identifier_subscription
|
||||
user.notification_subscriptions.create(params.merge(identifier: identifier))
|
||||
end
|
||||
end
|
||||
@@ -1,77 +0,0 @@
|
||||
class ReportBuilder
|
||||
include CustomExceptions::Report
|
||||
|
||||
# Usage
|
||||
# rb = ReportBuilder.new a, { metric: 'conversations_count', type: :account, id: 1}
|
||||
# rb = ReportBuilder.new a, { metric: 'avg_first_response_time', type: :agent, id: 1}
|
||||
|
||||
IDENTITY_MAPPING = {
|
||||
account: AccountIdentity,
|
||||
agent: AgentIdentity
|
||||
}.freeze
|
||||
|
||||
def initialize(account, params)
|
||||
@account = account
|
||||
@params = params
|
||||
@identity = get_identity
|
||||
@start_time, @end_time = validate_times
|
||||
end
|
||||
|
||||
def build
|
||||
metric = @identity.send(@params[:metric])
|
||||
if metric.get.nil?
|
||||
metric.delete
|
||||
result = {}
|
||||
else
|
||||
result = metric.get_padded_range(@start_time, @end_time) || {}
|
||||
end
|
||||
formatted_hash(result)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_identity
|
||||
identity_class = IDENTITY_MAPPING[@params[:type]]
|
||||
raise InvalidIdentity if identity_class.nil?
|
||||
|
||||
@params[:id] = @account.id if identity_class == AccountIdentity
|
||||
identity_id = @params[:id]
|
||||
raise IdentityNotFound if identity_id.nil?
|
||||
|
||||
tags = identity_class == AccountIdentity ? nil : { account_id: @account.id }
|
||||
identity = identity_class.new(identity_id, tags: tags)
|
||||
raise MetricNotFound if @params[:metric].blank?
|
||||
raise MetricNotFound unless identity.respond_to?(@params[:metric])
|
||||
|
||||
identity
|
||||
end
|
||||
|
||||
def validate_times
|
||||
start_time = @params[:since] || Time.now.end_of_day - 30.days
|
||||
end_time = @params[:until] || Time.now.end_of_day
|
||||
start_time = begin
|
||||
parse_date_time(start_time)
|
||||
rescue StandardError
|
||||
raise(InvalidStartTime)
|
||||
end
|
||||
end_time = begin
|
||||
parse_date_time(end_time)
|
||||
rescue StandardError
|
||||
raise(InvalidEndTime)
|
||||
end
|
||||
[start_time, end_time]
|
||||
end
|
||||
|
||||
def parse_date_time(datetime)
|
||||
return datetime if datetime.is_a?(DateTime)
|
||||
return datetime.to_datetime if datetime.is_a?(Time) || datetime.is_a?(Date)
|
||||
|
||||
DateTime.strptime(datetime, '%s')
|
||||
end
|
||||
|
||||
def formatted_hash(hash)
|
||||
hash.each_with_object([]) do |p, arr|
|
||||
arr << { value: p[1], timestamp: p[0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -50,14 +50,15 @@ class V2::ReportBuilder
|
||||
.count
|
||||
end
|
||||
|
||||
# unscoped removes all scopes added to a model previously
|
||||
def incoming_messages_count
|
||||
scope.messages.unscoped.incoming
|
||||
scope.messages.unscoped.where(account_id: account.id).incoming
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
def outgoing_messages_count
|
||||
scope.messages.unscoped.outgoing
|
||||
scope.messages.unscoped.where(account_id: account.id).outgoing
|
||||
.group_by_day(:created_at, range: range, default_value: 0)
|
||||
.count
|
||||
end
|
||||
|
||||
@@ -8,7 +8,7 @@ class Api::BaseController < ApplicationController
|
||||
private
|
||||
|
||||
def authenticate_by_access_token?
|
||||
request.headers[:api_access_token].present?
|
||||
request.headers[:api_access_token].present? || request.headers[:HTTP_API_ACCESS_TOKEN].present?
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
|
||||
@@ -31,7 +31,7 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def update
|
||||
@account.update!(account_params.slice(:name, :locale))
|
||||
@account.update!(account_params.slice(:name, :locale, :domain, :support_email, :domain_emails_enabled))
|
||||
end
|
||||
|
||||
private
|
||||
@@ -45,7 +45,7 @@ class Api::V1::Accounts::AccountsController < Api::BaseController
|
||||
end
|
||||
|
||||
def account_params
|
||||
params.permit(:account_name, :email, :name, :locale)
|
||||
params.permit(:account_name, :email, :name, :locale, :domain, :support_email, :domain_emails_enabled)
|
||||
end
|
||||
|
||||
def check_signup_enabled
|
||||
|
||||
@@ -4,16 +4,18 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
|
||||
def register_facebook_page
|
||||
user_access_token = params[:user_access_token]
|
||||
page_access_token = params[:page_access_token]
|
||||
page_name = params[:page_name]
|
||||
page_id = params[:page_id]
|
||||
inbox_name = params[:inbox_name]
|
||||
facebook_channel = current_account.facebook_pages.create!(
|
||||
name: page_name, page_id: page_id, user_access_token: user_access_token,
|
||||
page_access_token: page_access_token
|
||||
)
|
||||
set_avatar(facebook_channel, page_id)
|
||||
inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
render json: inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
facebook_channel = current_account.facebook_pages.create!(
|
||||
page_id: page_id, user_access_token: user_access_token,
|
||||
page_access_token: page_access_token
|
||||
)
|
||||
@facebook_inbox = current_account.inboxes.create!(name: inbox_name, channel: facebook_channel)
|
||||
set_avatar(@facebook_inbox, page_id)
|
||||
rescue StandardError => e
|
||||
Rails.logger e
|
||||
end
|
||||
end
|
||||
|
||||
def facebook_pages
|
||||
@@ -72,13 +74,13 @@ class Api::V1::Accounts::CallbacksController < Api::BaseController
|
||||
end
|
||||
end
|
||||
|
||||
def set_avatar(facebook_channel, page_id)
|
||||
def set_avatar(facebook_inbox, page_id)
|
||||
uri = get_avatar_url(page_id)
|
||||
|
||||
return unless uri
|
||||
|
||||
avatar_resource = LocalResource.new(uri)
|
||||
facebook_channel.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
||||
facebook_inbox.avatar.attach(io: avatar_resource.file, filename: avatar_resource.tmp_filename, content_type: avatar_resource.encoding)
|
||||
end
|
||||
|
||||
def get_avatar_url(page_id)
|
||||
|
||||
@@ -2,13 +2,15 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseControlle
|
||||
before_action :authorize_request
|
||||
|
||||
def create
|
||||
authenticate_twilio
|
||||
build_inbox
|
||||
setup_webhooks
|
||||
rescue Twilio::REST::TwilioError => e
|
||||
render_could_not_create_error(e.message)
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
ActiveRecord::Base.transaction do
|
||||
authenticate_twilio
|
||||
build_inbox
|
||||
setup_webhooks if @twilio_channel.sms?
|
||||
rescue Twilio::REST::TwilioError => e
|
||||
render_could_not_create_error(e.message)
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
@@ -26,25 +28,30 @@ class Api::V1::Accounts::Channels::TwilioChannelsController < Api::BaseControlle
|
||||
::Twilio::WebhookSetupService.new(inbox: @inbox).perform
|
||||
end
|
||||
|
||||
def phone_number
|
||||
medium == 'sms' ? permitted_params[:phone_number] : "whatsapp:#{permitted_params[:phone_number]}"
|
||||
end
|
||||
|
||||
def medium
|
||||
permitted_params[:medium]
|
||||
end
|
||||
|
||||
def build_inbox
|
||||
ActiveRecord::Base.transaction do
|
||||
twilio_sms = current_account.twilio_sms.create(
|
||||
account_sid: permitted_params[:account_sid],
|
||||
auth_token: permitted_params[:auth_token],
|
||||
phone_number: permitted_params[:phone_number]
|
||||
)
|
||||
@inbox = current_account.inboxes.create(
|
||||
name: permitted_params[:name],
|
||||
channel: twilio_sms
|
||||
)
|
||||
rescue StandardError => e
|
||||
render_could_not_create_error(e.message)
|
||||
end
|
||||
@twilio_channel = current_account.twilio_sms.create!(
|
||||
account_sid: permitted_params[:account_sid],
|
||||
auth_token: permitted_params[:auth_token],
|
||||
phone_number: phone_number,
|
||||
medium: medium
|
||||
)
|
||||
@inbox = current_account.inboxes.create(
|
||||
name: permitted_params[:name],
|
||||
channel: @twilio_channel
|
||||
)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.require(:twilio_channel).permit(
|
||||
:account_id, :phone_number, :account_sid, :auth_token, :name
|
||||
:account_id, :phone_number, :account_sid, :auth_token, :name, :medium
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,11 +4,6 @@ class Api::V1::Accounts::ContactsController < Api::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_contact, only: [:show, :update]
|
||||
|
||||
skip_before_action :authenticate_user!, only: [:create]
|
||||
skip_before_action :set_current_user, only: [:create]
|
||||
skip_before_action :check_subscription, only: [:create]
|
||||
skip_around_action :handle_with_exception, only: [:create]
|
||||
|
||||
def index
|
||||
@contacts = current_account.contacts
|
||||
end
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
class Api::V1::Accounts::ConversationsController < Api::BaseController
|
||||
include Events::Types
|
||||
before_action :conversation, except: [:index]
|
||||
before_action :contact_inbox, only: [:create]
|
||||
|
||||
def index
|
||||
result = conversation_finder.perform
|
||||
@@ -7,12 +9,30 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
|
||||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def meta
|
||||
result = conversation_finder.perform
|
||||
@conversations_count = result[:count]
|
||||
end
|
||||
|
||||
def create
|
||||
@conversation = ::Conversation.create!(conversation_params)
|
||||
end
|
||||
|
||||
def show; end
|
||||
|
||||
def toggle_status
|
||||
@status = @conversation.toggle_status
|
||||
end
|
||||
|
||||
def toggle_typing_status
|
||||
if params[:typing_status] == 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
elsif params[:typing_status] == 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def update_last_seen
|
||||
@conversation.agent_last_seen_at = parsed_last_seen_at
|
||||
@conversation.save!
|
||||
@@ -21,6 +41,11 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
|
||||
|
||||
private
|
||||
|
||||
def trigger_typing_event(event)
|
||||
user = current_user.presence || @resource
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
|
||||
end
|
||||
|
||||
def parsed_last_seen_at
|
||||
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
|
||||
end
|
||||
@@ -29,6 +54,19 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
|
||||
@conversation ||= current_account.conversations.find_by(display_id: params[:id])
|
||||
end
|
||||
|
||||
def contact_inbox
|
||||
@contact_inbox ||= ::ContactInbox.find_by!(source_id: params[:source_id])
|
||||
end
|
||||
|
||||
def conversation_params
|
||||
{
|
||||
account_id: current_account.id,
|
||||
inbox_id: @contact_inbox.inbox_id,
|
||||
contact_id: @contact_inbox.contact_id,
|
||||
contact_inbox_id: @contact_inbox.id
|
||||
}
|
||||
end
|
||||
|
||||
def conversation_finder
|
||||
@conversation_finder ||= ConversationFinder.new(current_user, params)
|
||||
end
|
||||
|
||||
@@ -1,13 +1,35 @@
|
||||
class Api::V1::Accounts::InboxesController < Api::BaseController
|
||||
before_action :check_authorization
|
||||
before_action :fetch_inbox, only: [:destroy, :update]
|
||||
before_action :fetch_inbox, except: [:index, :create]
|
||||
before_action :fetch_agent_bot, only: [:set_agent_bot]
|
||||
|
||||
def index
|
||||
@inboxes = policy_scope(current_account.inboxes)
|
||||
end
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = web_widgets.create!(permitted_params[:channel].except(:type)) if permitted_params[:channel][:type] == 'web_widget'
|
||||
@inbox = current_account.inboxes.build(name: permitted_params[:name], channel: channel)
|
||||
@inbox.avatar.attach(permitted_params[:avatar])
|
||||
@inbox.save!
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@inbox.update(inbox_update_params)
|
||||
@inbox.update(inbox_update_params.except(:channel))
|
||||
@inbox.channel.update!(inbox_update_params[:channel]) if @inbox.channel.is_a?(Channel::WebWidget) && inbox_update_params[:channel].present?
|
||||
end
|
||||
|
||||
def set_agent_bot
|
||||
if @agent_bot
|
||||
agent_bot_inbox = @inbox.agent_bot_inbox || AgentBotInbox.new(inbox: @inbox)
|
||||
agent_bot_inbox.agent_bot = @agent_bot
|
||||
agent_bot_inbox.save!
|
||||
elsif @inbox.agent_bot_inbox.present?
|
||||
@inbox.agent_bot_inbox.destroy!
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
@@ -21,11 +43,24 @@ class Api::V1::Accounts::InboxesController < Api::BaseController
|
||||
@inbox = current_account.inboxes.find(params[:id])
|
||||
end
|
||||
|
||||
def fetch_agent_bot
|
||||
@agent_bot = AgentBot.find(params[:agent_bot]) if params[:agent_bot]
|
||||
end
|
||||
|
||||
def web_widgets
|
||||
current_account.web_widgets
|
||||
end
|
||||
|
||||
def check_authorization
|
||||
authorize(Inbox)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :avatar, :name, channel: [:type, :website_url, :widget_color, :welcome_title, :welcome_tagline, :agent_away_message])
|
||||
end
|
||||
|
||||
def inbox_update_params
|
||||
params.require(:inbox).permit(:enable_auto_assignment)
|
||||
params.permit(:enable_auto_assignment, :name, :avatar, channel: [:website_url, :widget_color, :welcome_title,
|
||||
:welcome_tagline, :agent_away_message])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,10 +20,11 @@ class Api::V1::Accounts::NotificationSettingsController < Api::BaseController
|
||||
end
|
||||
|
||||
def notification_setting_params
|
||||
params.require(:notification_settings).permit(selected_email_flags: [])
|
||||
params.require(:notification_settings).permit(selected_email_flags: [], selected_push_flags: [])
|
||||
end
|
||||
|
||||
def update_flags
|
||||
@notification_setting.selected_email_flags = notification_setting_params[:selected_email_flags]
|
||||
@notification_setting.selected_push_flags = notification_setting_params[:selected_push_flags]
|
||||
end
|
||||
end
|
||||
|
||||
21
app/controllers/api/v1/accounts/notifications_controller.rb
Normal file
21
app/controllers/api/v1/accounts/notifications_controller.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class Api::V1::Accounts::NotificationsController < Api::BaseController
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
before_action :fetch_notification, only: [:update]
|
||||
|
||||
def index
|
||||
@notifications = current_user.notifications.where(account_id: current_account.id)
|
||||
render json: @notifications
|
||||
end
|
||||
|
||||
def update
|
||||
@notification.update(read_at: DateTime.now.utc)
|
||||
render json: @notification
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_notification
|
||||
@notification = current_user.notifications.find(params[:id])
|
||||
end
|
||||
end
|
||||
@@ -1,99 +0,0 @@
|
||||
class Api::V1::Accounts::ReportsController < Api::BaseController
|
||||
include CustomExceptions::Report
|
||||
include Constants::Report
|
||||
|
||||
around_action :report_exception
|
||||
|
||||
def account
|
||||
builder = ReportBuilder.new(current_account, account_report_params)
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def agent
|
||||
builder = ReportBuilder.new(current_account, agent_report_params)
|
||||
data = builder.build
|
||||
render json: data
|
||||
end
|
||||
|
||||
def account_summary
|
||||
render json: account_summary_metrics
|
||||
end
|
||||
|
||||
def agent_summary
|
||||
render json: agent_summary_metrics
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def report_exception
|
||||
yield
|
||||
rescue InvalidIdentity, IdentityNotFound, MetricNotFound, InvalidStartTime, InvalidEndTime => e
|
||||
render_error_response(e)
|
||||
end
|
||||
|
||||
def current_account
|
||||
current_user.account
|
||||
end
|
||||
|
||||
def account_summary_metrics
|
||||
summary_metrics(ACCOUNT_METRICS, :account_summary_params, AVG_ACCOUNT_METRICS)
|
||||
end
|
||||
|
||||
def agent_summary_metrics
|
||||
summary_metrics(AGENT_METRICS, :agent_summary_params, AVG_AGENT_METRICS)
|
||||
end
|
||||
|
||||
def summary_metrics(metrics, calc_function, avg_metrics)
|
||||
metrics.each_with_object({}) do |metric, result|
|
||||
data = ReportBuilder.new(current_account, send(calc_function, metric)).build
|
||||
result[metric] = calculate_metric(data, metric, avg_metrics)
|
||||
end
|
||||
end
|
||||
|
||||
def calculate_metric(data, metric, avg_metrics)
|
||||
sum = data.inject(0) { |val, hash| val + hash[:value].to_i }
|
||||
if avg_metrics.include?(metric)
|
||||
sum /= data.length unless sum.zero?
|
||||
end
|
||||
sum
|
||||
end
|
||||
|
||||
def account_summary_params(metric)
|
||||
{
|
||||
metric: metric.to_s,
|
||||
type: :account,
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
|
||||
def agent_summary_params(metric)
|
||||
{
|
||||
metric: metric.to_s,
|
||||
type: :agent,
|
||||
since: params[:since],
|
||||
until: params[:until],
|
||||
id: params[:id]
|
||||
}
|
||||
end
|
||||
|
||||
def account_report_params
|
||||
{
|
||||
metric: params[:metric],
|
||||
type: :account,
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
|
||||
def agent_report_params
|
||||
{
|
||||
metric: params[:metric],
|
||||
type: :agent,
|
||||
id: params[:id],
|
||||
since: params[:since],
|
||||
until: params[:until]
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
class Api::V1::Accounts::Widget::InboxesController < Api::BaseController
|
||||
before_action :authorize_request
|
||||
before_action :set_web_widget_channel, only: [:update]
|
||||
before_action :set_inbox, only: [:update]
|
||||
|
||||
def create
|
||||
ActiveRecord::Base.transaction do
|
||||
channel = web_widgets.create!(
|
||||
website_name: permitted_params[:website][:website_name],
|
||||
website_url: permitted_params[:website][:website_url],
|
||||
widget_color: permitted_params[:website][:widget_color]
|
||||
)
|
||||
@inbox = inboxes.create!(name: permitted_params[:website][:website_name], channel: channel)
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
@channel.update!(
|
||||
widget_color: permitted_params[:website][:widget_color]
|
||||
)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_request
|
||||
authorize ::Inbox
|
||||
end
|
||||
|
||||
def inboxes
|
||||
current_account.inboxes
|
||||
end
|
||||
|
||||
def web_widgets
|
||||
current_account.web_widgets
|
||||
end
|
||||
|
||||
def set_web_widget_channel
|
||||
@channel = web_widgets.find_by(id: permitted_params[:id])
|
||||
end
|
||||
|
||||
def set_inbox
|
||||
@inbox = @channel.inbox
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, website: [:website_name, :website_url, :widget_color])
|
||||
end
|
||||
end
|
||||
8
app/controllers/api/v1/agent_bots_controller.rb
Normal file
8
app/controllers/api/v1/agent_bots_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
class Api::V1::AgentBotsController < Api::BaseController
|
||||
skip_before_action :authenticate_user!
|
||||
skip_before_action :check_subscription
|
||||
|
||||
def index
|
||||
render json: AgentBot.all
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
class Api::V1::NotificationSubscriptionsController < Api::BaseController
|
||||
before_action :set_user
|
||||
|
||||
def create
|
||||
notification_subscription = NotificationSubscriptionBuilder.new(user: @user, params: notification_subscription_params).perform
|
||||
|
||||
render json: notification_subscription
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user
|
||||
@user = current_user
|
||||
end
|
||||
|
||||
def notification_subscription_params
|
||||
params.require(:notification_subscription).permit(:subscription_type, subscription_attributes: {})
|
||||
end
|
||||
end
|
||||
@@ -2,9 +2,9 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
private
|
||||
|
||||
def conversation
|
||||
@conversation ||= @contact_inbox.conversations.find_by(
|
||||
@conversation ||= @contact_inbox.conversations.where(
|
||||
inbox_id: auth_token_params[:inbox_id]
|
||||
)
|
||||
).last
|
||||
end
|
||||
|
||||
def auth_token_params
|
||||
@@ -18,6 +18,7 @@ class Api::V1::Widget::BaseController < ApplicationController
|
||||
def set_web_widget
|
||||
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
|
||||
@account = @web_widget.account
|
||||
switch_locale @account
|
||||
end
|
||||
|
||||
def set_contact
|
||||
|
||||
27
app/controllers/api/v1/widget/conversations_controller.rb
Normal file
27
app/controllers/api/v1/widget/conversations_controller.rb
Normal file
@@ -0,0 +1,27 @@
|
||||
class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
|
||||
def toggle_typing
|
||||
head :ok && return if conversation.nil?
|
||||
|
||||
if permitted_params[:typing_status] == 'on'
|
||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||
elsif permitted_params[:typing_status] == 'off'
|
||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||
end
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trigger_typing_event(event)
|
||||
Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :typing_status, :website_token)
|
||||
end
|
||||
end
|
||||
16
app/controllers/api/v1/widget/events_controller.rb
Normal file
16
app/controllers/api/v1/widget/events_controller.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
class Api::V1::Widget::EventsController < Api::V1::Widget::BaseController
|
||||
include Events::Types
|
||||
before_action :set_web_widget
|
||||
before_action :set_contact
|
||||
|
||||
def create
|
||||
Rails.configuration.dispatcher.dispatch(permitted_params[:name], Time.zone.now, contact_inbox: @contact_inbox)
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:name, :website_token)
|
||||
end
|
||||
end
|
||||
@@ -10,13 +10,17 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
|
||||
def create
|
||||
@message = conversation.messages.new(message_params)
|
||||
@message.save
|
||||
build_attachment
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def update
|
||||
@message.update!(input_submitted_email: contact_email)
|
||||
update_contact(contact_email)
|
||||
if @message.content_type == 'input_email'
|
||||
@message.update!(submitted_email: contact_email)
|
||||
update_contact(contact_email)
|
||||
else
|
||||
@message.update!(message_update_params[:message])
|
||||
end
|
||||
rescue StandardError => e
|
||||
render json: { error: @contact.errors, message: e.message }.to_json, status: 500
|
||||
end
|
||||
@@ -24,13 +28,16 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
private
|
||||
|
||||
def build_attachment
|
||||
return if params[:message][:attachment].blank?
|
||||
return if params[:message][:attachments].blank?
|
||||
|
||||
@message.attachment = Attachment.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(params[:message][:attachment][:file]&.content_type)
|
||||
)
|
||||
@message.attachment.file.attach(params[:message][:attachment][:file])
|
||||
params[:message][:attachments].each do |uploaded_attachment|
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
file_type: helpers.file_type(uploaded_attachment&.content_type)
|
||||
)
|
||||
attachment.file.attach(uploaded_attachment)
|
||||
end
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def set_conversation
|
||||
@@ -116,6 +123,10 @@ class Api::V1::Widget::MessagesController < Api::V1::Widget::BaseController
|
||||
contact_email.split('@')[0]
|
||||
end
|
||||
|
||||
def message_update_params
|
||||
params.permit(message: [submitted_values: [:name, :title, :value]])
|
||||
end
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id, :before, :website_token, contact: [:email], message: [:content, :referer_url, :timestamp])
|
||||
end
|
||||
|
||||
8
app/controllers/api_controller.rb
Normal file
8
app/controllers/api_controller.rb
Normal file
@@ -0,0 +1,8 @@
|
||||
class ApiController < ApplicationController
|
||||
skip_before_action :set_current_user, only: [:index]
|
||||
skip_before_action :check_subscription, only: [:index]
|
||||
|
||||
def index
|
||||
render json: { version: Chatwoot.config[:version], timestamp: Time.now.utc.to_formatted_s(:db) }
|
||||
end
|
||||
end
|
||||
6
app/controllers/apple_app_controller.rb
Normal file
6
app/controllers/apple_app_controller.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class AppleAppController < ApplicationController
|
||||
def site_association
|
||||
site_association_json = render_to_string action: 'site_association', layout: false
|
||||
send_data site_association_json, filename: 'apple-app-site-association', type: 'application/json'
|
||||
end
|
||||
end
|
||||
@@ -24,9 +24,14 @@ class ApplicationController < ActionController::Base
|
||||
elsif @resource&.is_a?(AgentBot)
|
||||
account_accessible_for_bot?(account)
|
||||
end
|
||||
switch_locale account
|
||||
account
|
||||
end
|
||||
|
||||
def switch_locale(account)
|
||||
I18n.locale = (I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil) || I18n.default_locale
|
||||
end
|
||||
|
||||
def account_accessible_for_user?(account)
|
||||
render_unauthorized('You are not authorized to access this account') unless account.account_users.find_by(user_id: current_user.id)
|
||||
end
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
module AccessTokenAuthHelper
|
||||
BOT_ACCESSIBLE_ENDPOINTS = {
|
||||
'api/v1/accounts/conversations' => ['toggle_status'],
|
||||
'api/v1/accounts/conversations' => %w[toggle_status create],
|
||||
'api/v1/accounts/conversations/messages' => ['create']
|
||||
}.freeze
|
||||
|
||||
def authenticate_access_token!
|
||||
access_token = AccessToken.find_by(token: request.headers[:api_access_token])
|
||||
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
|
||||
access_token = AccessToken.find_by(token: token)
|
||||
render_unauthorized('Invalid Access Token') && return unless access_token
|
||||
|
||||
token_owner = access_token.owner
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
require 'rest-client'
|
||||
require 'telegram/bot'
|
||||
|
||||
class HomeController < ApplicationController
|
||||
skip_before_action :verify_authenticity_token, only: [:telegram]
|
||||
skip_before_action :authenticate_user!, only: [:telegram], raise: false
|
||||
skip_before_action :set_current_user
|
||||
skip_before_action :check_subscription
|
||||
end
|
||||
@@ -23,7 +23,9 @@ class Twilio::CallbackController < ApplicationController
|
||||
:FromZip,
|
||||
:Body,
|
||||
:ToCountry,
|
||||
:FromState
|
||||
:FromState,
|
||||
:MediaUrl0,
|
||||
:MediaContentType0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class Twitter::AuthorizationsController < Twitter::BaseController
|
||||
::Redis::Alfred.setex(oauth_token, account.id)
|
||||
redirect_to oauth_authorize_endpoint(oauth_token)
|
||||
else
|
||||
redirect_to app_new_twitter_inbox_url
|
||||
redirect_to app_new_twitter_inbox_url(account_id: account.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -39,8 +39,7 @@ class Twitter::CallbacksController < Twitter::BaseController
|
||||
twitter_profile = account.twitter_profiles.create(
|
||||
twitter_access_token: parsed_body['oauth_token'],
|
||||
twitter_access_token_secret: parsed_body['oauth_token_secret'],
|
||||
profile_id: parsed_body['user_id'],
|
||||
name: parsed_body['screen_name']
|
||||
profile_id: parsed_body['user_id']
|
||||
)
|
||||
account.inboxes.create(
|
||||
name: parsed_body['screen_name'],
|
||||
|
||||
@@ -9,8 +9,7 @@ class AsyncDispatcher < BaseDispatcher
|
||||
end
|
||||
|
||||
def listeners
|
||||
listeners = [AgentBotListener.instance, EmailNotificationListener.instance, ReportingListener.instance, WebhookListener.instance]
|
||||
listeners << EventListener.instance
|
||||
listeners = [EventListener.instance, WebhookListener.instance]
|
||||
listeners << SubscriptionListener.instance if ENV['BILLING_ENABLED']
|
||||
listeners
|
||||
end
|
||||
|
||||
@@ -5,6 +5,6 @@ class SyncDispatcher < BaseDispatcher
|
||||
end
|
||||
|
||||
def listeners
|
||||
[ActionCableListener.instance]
|
||||
[ActionCableListener.instance, AgentBotListener.instance, NotificationListener.instance]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,7 +11,7 @@ class MessageFinder
|
||||
private
|
||||
|
||||
def conversation_messages
|
||||
@conversation.messages.includes(:attachment, user: { avatar_attachment: :blob })
|
||||
@conversation.messages.includes(:attachments, user: { avatar_attachment: :blob })
|
||||
end
|
||||
|
||||
def messages
|
||||
|
||||
@@ -1,2 +1,5 @@
|
||||
module ApplicationHelper
|
||||
def available_locales_with_name
|
||||
LANGUAGES_CONFIG.map { |_key, val| val.slice(:name, :iso_639_1_code) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
class AccountIdentity < Nightfury::Identity::Base
|
||||
metric :conversations_count, :count_time_series, store_as: :b, step: :day
|
||||
metric :incoming_messages_count, :count_time_series, step: :day
|
||||
metric :outgoing_messages_count, :count_time_series, step: :day
|
||||
metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day
|
||||
metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day
|
||||
metric :resolutions_count, :count_time_series, store_as: :g, step: :day
|
||||
end
|
||||
|
||||
AccountIdentity.store_as = :ci
|
||||
@@ -1,8 +0,0 @@
|
||||
class AgentIdentity < Nightfury::Identity::Base
|
||||
metric :avg_first_response_time, :avg_time_series, store_as: :d, step: :day
|
||||
metric :avg_resolution_time, :avg_time_series, store_as: :f, step: :day
|
||||
metric :resolutions_count, :count_time_series, store_as: :g, step: :day
|
||||
tag :account_id, store_as: :co
|
||||
end
|
||||
|
||||
AgentIdentity.store_as = :ai
|
||||
@@ -2,7 +2,7 @@ import ApiClient from '../ApiClient';
|
||||
|
||||
class WebChannel extends ApiClient {
|
||||
constructor() {
|
||||
super('widget/inboxes', { accountScoped: true });
|
||||
super('inboxes', { accountScoped: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,12 @@ class ConversationApi extends ApiClient {
|
||||
agent_last_seen_at: lastSeen,
|
||||
});
|
||||
}
|
||||
|
||||
toggleTyping({ conversationId, status }) {
|
||||
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
|
||||
typing_status: status,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new ConversationApi();
|
||||
|
||||
@@ -9,7 +9,7 @@ class MessageApi extends ApiClient {
|
||||
|
||||
create({ conversationId, message, private: isPrivate }) {
|
||||
return axios.post(`${this.url}/${conversationId}/messages`, {
|
||||
message,
|
||||
content: message,
|
||||
private: isPrivate,
|
||||
});
|
||||
}
|
||||
@@ -22,7 +22,7 @@ class MessageApi extends ApiClient {
|
||||
|
||||
sendAttachment([conversationId, { file }]) {
|
||||
const formData = new FormData();
|
||||
formData.append('attachment[file]', file);
|
||||
formData.append('attachments[]', file, file.name);
|
||||
return axios({
|
||||
method: 'post',
|
||||
url: `${this.url}/${conversationId}/messages`,
|
||||
|
||||
9
app/javascript/dashboard/api/notificationSubscription.js
Normal file
9
app/javascript/dashboard/api/notificationSubscription.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class NotificationSubscriptions extends ApiClient {
|
||||
constructor() {
|
||||
super('notification_subscriptions');
|
||||
}
|
||||
}
|
||||
|
||||
export default new NotificationSubscriptions();
|
||||
BIN
app/javascript/dashboard/assets/images/channels/whatsapp.png
Normal file
BIN
app/javascript/dashboard/assets/images/channels/whatsapp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 46 KiB |
BIN
app/javascript/dashboard/assets/images/typing.gif
Normal file
BIN
app/javascript/dashboard/assets/images/typing.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
@@ -49,10 +49,10 @@ $global-font-size: 10px;
|
||||
$global-width: 100%;
|
||||
$global-lineheight: 1.5;
|
||||
$foundation-palette: (primary: $color-woot,
|
||||
secondary: #777,
|
||||
success: #13ce66,
|
||||
warning: #ffc82c,
|
||||
alert: #ff4949);
|
||||
secondary: #35c5ff,
|
||||
success: #44ce4b,
|
||||
warning: #ffc532,
|
||||
alert: #ff382d);
|
||||
$light-gray: #c0ccda;
|
||||
$medium-gray: #8492a6;
|
||||
$dark-gray: $color-gray;
|
||||
@@ -127,7 +127,7 @@ $header-styles: (small: ("h1": ("font-size": 24),
|
||||
$header-text-rendering: optimizeLegibility;
|
||||
$small-font-size: 80%;
|
||||
$header-small-font-color: $medium-gray;
|
||||
$paragraph-lineheight: 1.6;
|
||||
$paragraph-lineheight: 1.45;
|
||||
$paragraph-margin-bottom: 1rem;
|
||||
$paragraph-text-rendering: optimizeLegibility;
|
||||
$code-color: $black;
|
||||
@@ -377,8 +377,8 @@ $form-button-radius: $global-radius;
|
||||
// 20. Label
|
||||
// ---------
|
||||
|
||||
$label-background: $primary-color;
|
||||
$label-color: $white;
|
||||
$label-background: lighten($primary-color, 40%);
|
||||
$label-color: $primary-color;
|
||||
$label-color-alt: $black;
|
||||
$label-palette: $foundation-palette;
|
||||
$label-font-size: $font-size-micro;
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
background: $color-white;
|
||||
border-radius: $space-large;
|
||||
left: 0;
|
||||
margin: $space-slab 0 auto;
|
||||
margin: $space-slab auto;
|
||||
padding: $space-normal;
|
||||
top: 0;
|
||||
|
||||
|
||||
@@ -46,7 +46,8 @@ $color-gray: #6e6f73;
|
||||
$color-light-gray: #999a9b;
|
||||
$color-border: #e0e6ed;
|
||||
$color-border-light: #f0f4f5;
|
||||
$color-background: #eff2f7;
|
||||
$color-background: #f4f6fb;
|
||||
$color-border-dark: #cad0d4;
|
||||
$color-background-light: #f9fafc;
|
||||
$color-white: #fff;
|
||||
$color-body: #3c4858;
|
||||
@@ -54,11 +55,10 @@ $color-heading: #1f2d3d;
|
||||
$color-extra-light-blue: #f5f7f9;
|
||||
|
||||
$primary-color: $color-woot;
|
||||
$secondary-color: #ff5216;
|
||||
$success-color: #13ce66;
|
||||
$warning-color: #ffc82c;
|
||||
$alert-color: #ff4949;
|
||||
|
||||
$secondary-color: #35c5ff;
|
||||
$success-color: #44ce4b;
|
||||
$warning-color: #ffc532;
|
||||
$alert-color: #ff382d;
|
||||
// Color-palettes
|
||||
|
||||
$color-primary-light: #c7e3ff;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
@import 'widgets/conv-header';
|
||||
@import 'widgets/conversation-card';
|
||||
@import 'widgets/conversation-view';
|
||||
@import 'widgets/emojiinput';
|
||||
@import 'widgets/forms';
|
||||
@import 'widgets/login';
|
||||
@import 'widgets/modal';
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@include padding($space-normal $space-two $zero);
|
||||
}
|
||||
}
|
||||
|
||||
// Conversation header - Light BG
|
||||
.settings-header {
|
||||
@include padding($space-small $space-normal);
|
||||
@@ -14,6 +15,7 @@
|
||||
@include border-normal-bottom;
|
||||
height: $header-height;
|
||||
min-height: $header-height;
|
||||
|
||||
// Resolve Button
|
||||
.button {
|
||||
@include margin(0);
|
||||
@@ -31,42 +33,39 @@
|
||||
.wizard-box {
|
||||
.item {
|
||||
@include padding($space-normal $space-normal $space-normal $space-medium);
|
||||
position: relative;
|
||||
@include background-light;
|
||||
cursor: pointer;
|
||||
|
||||
&:before,
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
background: $color-border;
|
||||
content: '';
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: $space-normal;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
&:before {
|
||||
top: $zero;
|
||||
&::before {
|
||||
height: $space-normal;
|
||||
top: $zero;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
&:before {
|
||||
&::before {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
&:after {
|
||||
&::after {
|
||||
height: $zero;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
// left: 1px;
|
||||
// @include background-white;
|
||||
// @include border-light;
|
||||
// border-right: 0;
|
||||
h3 {
|
||||
color: $color-woot;
|
||||
}
|
||||
@@ -78,7 +77,7 @@
|
||||
|
||||
&.over {
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
background: $color-woot;
|
||||
}
|
||||
|
||||
@@ -87,17 +86,17 @@
|
||||
}
|
||||
|
||||
&+.item {
|
||||
&:before {
|
||||
&::before {
|
||||
background: $color-woot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $font-size-default;
|
||||
padding-left: $space-medium;
|
||||
line-height: 1;
|
||||
color: $color-body;
|
||||
font-size: $font-size-default;
|
||||
line-height: 1;
|
||||
padding-left: $space-medium;
|
||||
|
||||
.completed {
|
||||
color: $success-color;
|
||||
@@ -105,25 +104,25 @@
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-size-small;
|
||||
color: $color-light-gray;
|
||||
padding-left: $space-medium;
|
||||
font-size: $font-size-small;
|
||||
margin: 0;
|
||||
padding-left: $space-medium;
|
||||
}
|
||||
|
||||
.step {
|
||||
position: absolute;
|
||||
left: $space-normal;
|
||||
top: $space-normal;
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-medium;
|
||||
background: $color-border;
|
||||
border-radius: 20px;
|
||||
width: $space-normal;
|
||||
color: $color-white;
|
||||
font-size: $font-size-micro;
|
||||
font-weight: $font-weight-medium;
|
||||
height: $space-normal;
|
||||
text-align: center;
|
||||
left: $space-normal;
|
||||
line-height: $space-normal;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
top: $space-normal;
|
||||
width: $space-normal;
|
||||
z-index: 999;
|
||||
|
||||
i {
|
||||
@@ -141,10 +140,6 @@
|
||||
}
|
||||
|
||||
.inoboxes-list {
|
||||
// @include margin(auto);
|
||||
// @include background-white;
|
||||
// @include border-light;
|
||||
// width: 50%;
|
||||
|
||||
.inbox-item {
|
||||
@include margin($space-normal);
|
||||
@@ -152,20 +147,23 @@
|
||||
@include flex-shrink;
|
||||
@include padding($space-normal $space-normal);
|
||||
@include border-light-bottom();
|
||||
flex-direction: column;
|
||||
|
||||
background: $color-white;
|
||||
cursor: pointer;
|
||||
width: 20%;
|
||||
flex-direction: column;
|
||||
float: left;
|
||||
min-height: 10rem;
|
||||
width: 20%;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $zero;
|
||||
@include border-nil;
|
||||
|
||||
margin-bottom: $zero;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@include background-gray;
|
||||
|
||||
.arrow {
|
||||
opacity: 1;
|
||||
transform: translateX($space-small);
|
||||
@@ -174,8 +172,8 @@
|
||||
|
||||
.switch {
|
||||
align-self: center;
|
||||
margin-right: $space-normal;
|
||||
margin-bottom: $zero;
|
||||
margin-right: $space-normal;
|
||||
}
|
||||
|
||||
.item--details {
|
||||
@@ -187,15 +185,15 @@
|
||||
}
|
||||
|
||||
.item--sub {
|
||||
margin-bottom: 0;
|
||||
font-size: $font-size-small;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
align-self: center;
|
||||
font-size: $font-size-small;
|
||||
color: $medium-gray;
|
||||
font-size: $font-size-small;
|
||||
opacity: .7;
|
||||
transform: translateX(0);
|
||||
transition: opacity 0.100s ease-in 0s, transform 0.200s ease-in 0.030s;
|
||||
@@ -204,18 +202,19 @@
|
||||
}
|
||||
|
||||
.settings--content {
|
||||
@include margin($space-small $space-medium);
|
||||
@include margin($space-small $space-larger);
|
||||
|
||||
.title {
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.code {
|
||||
@include padding($space-one);
|
||||
|
||||
background: $color-background;
|
||||
max-height: $space-mega;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
@include padding($space-one);
|
||||
background: $color-background;
|
||||
|
||||
code {
|
||||
background: transparent;
|
||||
@@ -225,14 +224,14 @@
|
||||
}
|
||||
|
||||
.login-init {
|
||||
text-align: center;
|
||||
padding-top: 30%;
|
||||
text-align: center;
|
||||
|
||||
p {
|
||||
@include padding($space-medium);
|
||||
}
|
||||
|
||||
> a > img {
|
||||
>a>img {
|
||||
width: $space-larger * 5;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.integrations-wrap {
|
||||
.integration {
|
||||
background: $color-white;
|
||||
border: 2px solid $color-border;
|
||||
border-radius: $space-slab;
|
||||
border: 1px solid $color-border;
|
||||
border-radius: $space-smaller;
|
||||
padding: $space-normal;
|
||||
|
||||
.integration--image {
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
.user--name {
|
||||
@include margin(0);
|
||||
font-size: $font-size-medium;
|
||||
line-height: 1.3;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
@@ -65,6 +66,8 @@
|
||||
}
|
||||
|
||||
.button.resolve--button {
|
||||
width: 13.2rem;
|
||||
|
||||
>.icon {
|
||||
font-size: $font-size-default;
|
||||
padding-right: $space-small;
|
||||
|
||||
@@ -1,23 +1,53 @@
|
||||
.conversation {
|
||||
@include flex;
|
||||
@include flex-shrink;
|
||||
@include padding($space-normal $zero $zero $space-normal);
|
||||
@include padding(0 0 0 $space-normal);
|
||||
align-items: center;
|
||||
border-bottom: 1px solid transparent;
|
||||
border-left: $space-micro solid transparent;
|
||||
border-top: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
||||
&.active {
|
||||
background: $color-background;
|
||||
border-bottom-color: $color-border-light;
|
||||
border-left-color: $color-woot;
|
||||
border-top-color: $color-border-light;
|
||||
|
||||
.conversation--details {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
+.conversation .conversation--details {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
.conversation--details {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-last-child(2) {
|
||||
.conversation--details {
|
||||
border-bottom-color: $color-border-light;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.conversation--details {
|
||||
@include margin($zero $zero $zero $space-one);
|
||||
@include margin(0 0 0 $space-one);
|
||||
@include border-light-bottom;
|
||||
@include padding($zero $zero $space-slab $zero);
|
||||
@include border-light-top;
|
||||
@include padding($space-slab 0);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.conversation--user {
|
||||
font-size: $font-size-small;
|
||||
margin-bottom: $zero;
|
||||
margin-bottom: 0;
|
||||
text-transform: capitalize;
|
||||
|
||||
.label {
|
||||
@@ -37,7 +67,7 @@
|
||||
font-weight: $font-weight-normal;
|
||||
height: $space-medium;
|
||||
line-height: $space-medium;
|
||||
margin: $zero;
|
||||
margin: 0;
|
||||
max-width: 96%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -52,20 +82,20 @@
|
||||
|
||||
.conversation--meta {
|
||||
@include flex;
|
||||
display: block;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
right: $space-normal;
|
||||
top: $space-normal;
|
||||
|
||||
.unread {
|
||||
$unread-size: $space-two - $space-micro;
|
||||
$unread-size: $space-normal;
|
||||
@include round-corner;
|
||||
@include light-shadow;
|
||||
background: darken($success-color, 3%);
|
||||
color: $color-white;
|
||||
display: none;
|
||||
font-size: $font-size-mini;
|
||||
font-weight: $font-weight-medium;
|
||||
font-size: $font-size-micro;
|
||||
font-weight: $font-weight-black;
|
||||
height: $unread-size;
|
||||
line-height: $unread-size;
|
||||
margin-left: auto;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
@mixin bubble-with-tyes {
|
||||
@include padding($space-smaller $space-one);
|
||||
@mixin bubble-with-types {
|
||||
@include padding($space-small $space-normal);
|
||||
@include margin($zero);
|
||||
background: $color-primary-light;
|
||||
border-radius: $space-small;
|
||||
color: $color-heading;
|
||||
background: $color-woot;
|
||||
border-radius: $space-one;
|
||||
color: $color-white;
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-normal;
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
@@ -15,6 +16,17 @@
|
||||
|
||||
.message-text__wrap {
|
||||
position: relative;
|
||||
|
||||
.time {
|
||||
color: $color-primary-light;
|
||||
display: block;
|
||||
font-size: $font-size-micro;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $color-white;
|
||||
}
|
||||
}
|
||||
|
||||
.message-text {
|
||||
@@ -51,8 +63,7 @@
|
||||
}
|
||||
|
||||
&::before {
|
||||
$color-black: #000;
|
||||
background-image: linear-gradient(-180deg, transparent 3%, $color-black 70%);
|
||||
background-image: linear-gradient(-180deg, transparent 3%, $color-heading 130%);
|
||||
bottom: 0;
|
||||
content: '';
|
||||
height: 20%;
|
||||
@@ -94,6 +105,7 @@
|
||||
|
||||
.load-more-conversations {
|
||||
font-size: $font-size-small;
|
||||
margin: 0;
|
||||
padding: $space-normal;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -122,10 +134,10 @@
|
||||
|
||||
.status--filter {
|
||||
@include padding($zero null $zero $space-normal);
|
||||
@include border-light;
|
||||
@include round-corner;
|
||||
@include margin($space-smaller $space-slab $zero $zero);
|
||||
background-color: $color-background;
|
||||
background-color: $color-background-light;
|
||||
border: 1px solid $color-border;
|
||||
float: right;
|
||||
font-size: $font-size-mini;
|
||||
height: $space-medium;
|
||||
@@ -192,168 +204,225 @@
|
||||
height: 100%;
|
||||
margin-bottom: $space-small;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
>li {
|
||||
@include flex;
|
||||
@include flex-shrink;
|
||||
@include margin($zero $zero $space-smaller);
|
||||
.conversation-panel>li {
|
||||
@include flex;
|
||||
@include flex-shrink;
|
||||
@include margin($zero $zero $space-micro);
|
||||
position: relative;
|
||||
|
||||
&:first-child {
|
||||
margin-top: auto;
|
||||
&:first-child {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $space-small;
|
||||
}
|
||||
|
||||
&.unread--toast {
|
||||
span {
|
||||
@include elegant-card;
|
||||
@include round-corner;
|
||||
background: $color-woot;
|
||||
color: $color-white;
|
||||
font-size: $font-size-mini;
|
||||
font-weight: $font-weight-medium;
|
||||
margin: $space-one auto;
|
||||
padding: $space-smaller $space-two;
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $space-small;
|
||||
.bubble {
|
||||
@include bubble-with-types;
|
||||
max-width: 50rem;
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
|
||||
.aplayer {
|
||||
box-shadow: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.unread--toast {
|
||||
span {
|
||||
@include elegant-card;
|
||||
@include round-corner;
|
||||
background: $color-woot;
|
||||
color: $color-white;
|
||||
font-size: $font-size-mini;
|
||||
font-weight: $font-weight-medium;
|
||||
margin: $space-one auto;
|
||||
padding: $space-smaller $space-two;
|
||||
&.left {
|
||||
|
||||
.bubble {
|
||||
@include border-normal;
|
||||
background: $white;
|
||||
border-bottom-left-radius: $space-smaller;
|
||||
border-top-left-radius: $space-smaller;
|
||||
color: $color-body;
|
||||
margin-right: auto;
|
||||
|
||||
.time {
|
||||
color: $color-light-gray;
|
||||
}
|
||||
|
||||
.image .time {
|
||||
color: $color-white;
|
||||
}
|
||||
|
||||
.link {
|
||||
color: $color-primary-dark;
|
||||
}
|
||||
|
||||
.file {
|
||||
.text-block-title {
|
||||
color: $color-body;
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
color: $color-woot;
|
||||
}
|
||||
|
||||
.download {
|
||||
color: $color-primary-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+.right {
|
||||
margin-top: $space-one;
|
||||
|
||||
.bubble {
|
||||
border-top-right-radius: $space-one;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.right {
|
||||
@include flex-align(right, null);
|
||||
|
||||
.wrap {
|
||||
margin-right: $space-normal;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
@include bubble-with-tyes;
|
||||
max-width: 50rem;
|
||||
text-align: left;
|
||||
word-wrap: break-word;
|
||||
border-bottom-right-radius: $space-smaller;
|
||||
border-top-right-radius: $space-smaller;
|
||||
margin-left: auto;
|
||||
|
||||
.aplayer {
|
||||
box-shadow: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&.left {
|
||||
.bubble {
|
||||
background: $white;
|
||||
border-bottom-left-radius: 0;
|
||||
border-top-left-radius: 0;
|
||||
&.is-private {
|
||||
background: lighten($warning-color, 32%);
|
||||
border: 1px solid $color-border;
|
||||
color: $color-heading;
|
||||
margin-right: auto;
|
||||
}
|
||||
padding-right: $space-large;
|
||||
position: relative;
|
||||
|
||||
+.right {
|
||||
margin-top: $space-one;
|
||||
&::before {
|
||||
bottom: 0;
|
||||
color: $medium-gray;
|
||||
position: absolute;
|
||||
right: $space-one;
|
||||
top: $space-smaller + $space-micro;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
border-top-right-radius: $space-small;
|
||||
.time {
|
||||
color: $color-light-gray;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&.right {
|
||||
@include flex-align(right, null);
|
||||
|
||||
.wrap {
|
||||
margin-right: $space-normal;
|
||||
text-align: right;
|
||||
}
|
||||
+.left {
|
||||
margin-top: $space-one;
|
||||
|
||||
.bubble {
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
margin-left: auto;
|
||||
|
||||
&.is-private {
|
||||
background: lighten($warning-color, 32%);
|
||||
color: $color-heading;
|
||||
padding-right: $space-large;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
bottom: 0;
|
||||
color: $medium-gray;
|
||||
position: absolute;
|
||||
right: $space-one;
|
||||
top: $space-smaller + $space-micro;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+.left {
|
||||
margin-top: $space-one;
|
||||
|
||||
.bubble {
|
||||
border-top-left-radius: $space-small;
|
||||
}
|
||||
border-top-left-radius: $space-one;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wrap {
|
||||
@include margin($zero $space-normal);
|
||||
max-width: 69%;
|
||||
.wrap {
|
||||
@include margin($zero $space-normal);
|
||||
max-width: 69%;
|
||||
|
||||
.sender--name {
|
||||
font-size: $font-size-mini;
|
||||
margin-bottom: $space-smaller;
|
||||
}
|
||||
.sender--name {
|
||||
font-size: $font-size-mini;
|
||||
margin-bottom: $space-smaller;
|
||||
}
|
||||
}
|
||||
|
||||
.sender--thumbnail {
|
||||
@include round-corner();
|
||||
height: $space-slab;
|
||||
margin-right: $space-one;
|
||||
margin-top: $space-micro;
|
||||
width: $space-slab;
|
||||
}
|
||||
.sender--thumbnail {
|
||||
@include round-corner();
|
||||
height: $space-slab;
|
||||
margin-right: $space-one;
|
||||
margin-top: $space-micro;
|
||||
width: $space-slab;
|
||||
}
|
||||
|
||||
.activity-wrap {
|
||||
@include flex;
|
||||
@include margin($space-small auto);
|
||||
@include padding($space-smaller $space-normal);
|
||||
@include flex-align($x: center, $y: null);
|
||||
background: lighten($warning-color, 32%);
|
||||
border: 1px solid lighten($warning-color, 26%);
|
||||
border-radius: $space-smaller;
|
||||
font-size: $font-size-small;
|
||||
.activity-wrap {
|
||||
@include flex;
|
||||
@include margin($space-small auto);
|
||||
@include padding($space-small $space-normal);
|
||||
@include flex-align($x: center, $y: null);
|
||||
background: lighten($warning-color, 32%);
|
||||
border: 1px solid lighten($warning-color, 22%);
|
||||
border-radius: $space-smaller;
|
||||
font-size: $font-size-small;
|
||||
|
||||
p {
|
||||
color: $color-heading;
|
||||
margin-bottom: $zero;
|
||||
p {
|
||||
color: $color-heading;
|
||||
margin-bottom: $zero;
|
||||
|
||||
.ion-person {
|
||||
color: $color-body;
|
||||
font-size: $font-size-default;
|
||||
margin-right: $space-small;
|
||||
position: relative;
|
||||
top: $space-micro;
|
||||
}
|
||||
|
||||
.message-text__wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
&::after {
|
||||
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
.ion-person {
|
||||
color: $color-body;
|
||||
font-size: $font-size-default;
|
||||
margin-right: $space-small;
|
||||
position: relative;
|
||||
top: $space-micro;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: $medium-gray;
|
||||
.message-text__wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
&::after {
|
||||
content: ' \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0';
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.time {
|
||||
bottom: -$space-micro;
|
||||
color: $color-gray;
|
||||
float: right;
|
||||
color: $medium-gray;
|
||||
font-size: $font-size-micro;
|
||||
font-style: italic;
|
||||
margin-left: $space-slab;
|
||||
right: -$space-micro;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.conversation-footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.typing-indicator-wrap {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
top: -$space-large;
|
||||
width: 100%;
|
||||
|
||||
.typing-indicator {
|
||||
@include elegant-card;
|
||||
@include round-corner;
|
||||
background: $color-white;
|
||||
color: $color-light-gray;
|
||||
font-size: $font-size-mini;
|
||||
font-weight: $font-weight-bold;
|
||||
margin: $space-one auto;
|
||||
padding: $space-small $space-normal $space-small $space-two;
|
||||
|
||||
.gif {
|
||||
margin-left: $space-small;
|
||||
width: $space-medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
@import '../variables';
|
||||
@import '../mixins';
|
||||
|
||||
.emoji-dialog {
|
||||
@include elegant-card;
|
||||
background: $color-white;
|
||||
@@ -15,15 +18,15 @@
|
||||
}
|
||||
|
||||
.emojione {
|
||||
@include margin($zero);
|
||||
font-size: $font-size-medium;
|
||||
font-size: $font-size-default;
|
||||
margin: $zero;
|
||||
}
|
||||
|
||||
.emoji-row {
|
||||
@include padding($space-small);
|
||||
box-sizing: border-box;
|
||||
height: 180px;
|
||||
overflow-y: auto;
|
||||
padding: $space-small;
|
||||
|
||||
.emoji {
|
||||
border-radius: 4px;
|
||||
@@ -52,27 +55,33 @@
|
||||
}
|
||||
|
||||
.emoji-dialog-header {
|
||||
@include padding($zero $space-smaller);
|
||||
background-color: $light-gray;
|
||||
background-color: $color-body;
|
||||
border-top-left-radius: $space-small;
|
||||
border-top-right-radius: $space-small;
|
||||
padding: $zero $space-smaller;
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: $space-smaller 0 0;
|
||||
|
||||
> li {
|
||||
@include padding($space-smaller $space-small);
|
||||
box-sizing: border-box;
|
||||
>li {
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 3.4rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
height: $space-medium;
|
||||
justify-content: center;
|
||||
padding: $space-smaller $space-small;
|
||||
}
|
||||
|
||||
> .active {
|
||||
background: $white;
|
||||
.emojione {
|
||||
height: $space-two;
|
||||
width: $space-normal;
|
||||
}
|
||||
|
||||
>.active {
|
||||
background: $color-white;
|
||||
border-top-left-radius: $space-small;
|
||||
border-top-right-radius: $space-small;
|
||||
}
|
||||
@@ -84,13 +93,14 @@
|
||||
}
|
||||
|
||||
.active {
|
||||
|
||||
img,
|
||||
svg {
|
||||
filter: grayscale(0);
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
>* {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
.error {
|
||||
#{$all-text-inputs},
|
||||
select,
|
||||
.multiselect > .multiselect__tags {
|
||||
@include thin-border( darken(get-color(alert), 25%));
|
||||
@include thin-border(darken(get-color(alert), 25%));
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
.message {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-top: -$space-normal;
|
||||
margin-bottom: $space-one;
|
||||
color: darken(get-color(alert), 25%);
|
||||
display: block;
|
||||
font-weight: $font-weight-normal;
|
||||
margin-bottom: $space-one;
|
||||
margin-top: -$space-normal;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +24,7 @@ input {
|
||||
}
|
||||
|
||||
.input-wrap {
|
||||
font-size: $font-size-small;
|
||||
color: $color-heading;
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,22 +42,26 @@
|
||||
font-size: $font-size-default;
|
||||
|
||||
input {
|
||||
padding: $space-slab;
|
||||
height: $space-larger;
|
||||
font-size: $font-size-default;
|
||||
height: $space-larger;
|
||||
padding: $space-slab;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: $font-size-small;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
height: $space-larger;
|
||||
}
|
||||
}
|
||||
|
||||
.sigin__footer {
|
||||
font-size: $font-size-default;
|
||||
padding: $space-medium;
|
||||
|
||||
> a {
|
||||
>a {
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
margin-bottom: $space-micro;
|
||||
margin-top: $space-micro;
|
||||
|
||||
>.inbox-icon {
|
||||
.inbox-icon {
|
||||
display: inline-block;
|
||||
margin-right: $space-micro;
|
||||
min-width: $space-normal;
|
||||
|
||||
@@ -1,33 +1,32 @@
|
||||
.ui-snackbar-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
margin: 0 auto;
|
||||
max-width: 40rem;
|
||||
overflow: hidden;
|
||||
z-index: 9999;
|
||||
top: $space-normal;
|
||||
left: $space-normal;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
top: $space-normal;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.ui-snackbar {
|
||||
text-align: left;
|
||||
@include padding($space-slab $space-medium);
|
||||
@include shadow;
|
||||
background-color: $woot-snackbar-bg;
|
||||
border-radius: $space-smaller;
|
||||
display: inline-block;
|
||||
min-width: 24rem;
|
||||
margin-bottom: $space-small;
|
||||
max-width: 40rem;
|
||||
min-height: 3rem;
|
||||
background-color: $woot-snackbar-bg;
|
||||
@include padding($space-slab $space-medium);
|
||||
@include border-top-radius($space-micro);
|
||||
@include border-right-radius($space-micro);
|
||||
@include border-bottom-radius($space-micro);
|
||||
@include border-left-radius($space-micro);
|
||||
margin-bottom: $space-small;
|
||||
|
||||
// box-shadow: 0 1px 3px alpha(black, 0.12), 0 1px 2px alpha(black, 0.24);
|
||||
min-width: 24rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ui-snackbar-text {
|
||||
font-size: $font-size-small;
|
||||
color: $color-white;
|
||||
font-size: $font-size-small;
|
||||
font-weight: $font-weight-medium;
|
||||
}
|
||||
|
||||
.ui-snackbar-action {
|
||||
@@ -35,12 +34,12 @@
|
||||
padding-left: 3rem;
|
||||
|
||||
button {
|
||||
@include margin(0);
|
||||
@include padding(0);
|
||||
background: none;
|
||||
border: 0;
|
||||
color: $woot-snackbar-button;
|
||||
font-size: $font-size-small;
|
||||
text-transform: uppercase;
|
||||
@include margin(0);
|
||||
@include padding(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
<template>
|
||||
<div class="row settings--form--header">
|
||||
<div class="medium-8">
|
||||
<p class="title">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p class="sub-head">
|
||||
{{ subTitle }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="buttonText" class="medium-4 text-right">
|
||||
<woot-submit-button
|
||||
class="default"
|
||||
:button-text="buttonText"
|
||||
:loading="isUpdating"
|
||||
@click="onClick()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
buttonText: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
isUpdating: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('update');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.settings--form--header {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid $color-border;
|
||||
display: flex;
|
||||
margin-bottom: $space-normal;
|
||||
padding: $space-normal 0;
|
||||
|
||||
.button {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
font-size: $font-size-default;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
app/javascript/dashboard/components/SettingsSection.vue
Normal file
46
app/javascript/dashboard/components/SettingsSection.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="row settings--section">
|
||||
<div class="medium-4">
|
||||
<p class="sub-block-title">
|
||||
{{ title }}
|
||||
</p>
|
||||
<p class="sub-head">
|
||||
{{ subTitle }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="medium-6">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
subTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '~dashboard/assets/scss/variables';
|
||||
|
||||
.settings--section {
|
||||
border-bottom: 1px solid $color-border;
|
||||
display: flex;
|
||||
padding: $space-normal 0;
|
||||
|
||||
.sub-block-title {
|
||||
color: $color-woot;
|
||||
font-weight: $font-weight-medium;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<transition-group name="toast-fade" tag="div" class="ui-snackbar-container">
|
||||
<woot-snackbar :message="snackMessage" v-for="snackMessage in snackMessages" v-bind:key="snackMessage" />
|
||||
<woot-snackbar
|
||||
v-for="snackMessage in snackMessages"
|
||||
:key="snackMessage"
|
||||
:message="snackMessage"
|
||||
/>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
@@ -9,8 +13,12 @@
|
||||
import WootSnackbar from './Snackbar';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WootSnackbar,
|
||||
},
|
||||
props: {
|
||||
duration: {
|
||||
type: Number,
|
||||
default: 2500,
|
||||
},
|
||||
},
|
||||
@@ -22,16 +30,12 @@ export default {
|
||||
},
|
||||
|
||||
mounted() {
|
||||
bus.$on('newToastMessage', (message) => {
|
||||
bus.$on('newToastMessage', message => {
|
||||
this.snackMessages.push(message);
|
||||
window.setTimeout(() => {
|
||||
this.snackMessages.splice(0, 1);
|
||||
}, this.duration);
|
||||
});
|
||||
},
|
||||
|
||||
components: {
|
||||
WootSnackbar,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<button
|
||||
type="submit"
|
||||
:type="type"
|
||||
:disabled="disabled"
|
||||
:class="computedClass"
|
||||
@click="onClick"
|
||||
@@ -39,6 +39,10 @@ export default {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: 'submit',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
computedClass() {
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/twitter-badge.png"
|
||||
/>
|
||||
|
||||
<img
|
||||
v-if="badge === 'Channel::TwilioSms'"
|
||||
id="badge"
|
||||
class="source-badge"
|
||||
:style="badgeStyle"
|
||||
src="~dashboard/assets/images/channels/whatsapp.png"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@@ -14,7 +14,7 @@ const chartOptions = {
|
||||
scales: {
|
||||
xAxes: [
|
||||
{
|
||||
barPercentage: 1.9,
|
||||
barPercentage: 1.26,
|
||||
ticks: {
|
||||
fontFamily,
|
||||
},
|
||||
@@ -27,6 +27,7 @@ const chartOptions = {
|
||||
{
|
||||
ticks: {
|
||||
fontFamily,
|
||||
beginAtZero: true,
|
||||
},
|
||||
gridLines: {
|
||||
display: false,
|
||||
|
||||
@@ -105,15 +105,17 @@ export default {
|
||||
router.push({ path: frontendURL(path) });
|
||||
},
|
||||
extractMessageText(chatItem) {
|
||||
if (chatItem.content) {
|
||||
return chatItem.content;
|
||||
const { content, attachments } = chatItem;
|
||||
|
||||
if (content) {
|
||||
return content;
|
||||
}
|
||||
let fileType = '';
|
||||
if (chatItem.attachment) {
|
||||
fileType = chatItem.attachment.file_type;
|
||||
} else {
|
||||
if (!attachments) {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
const [attachment] = attachments;
|
||||
const { file_type: fileType } = attachment;
|
||||
const key = `CHAT_LIST.ATTACHMENTS.${fileType}`;
|
||||
return `
|
||||
<i class="small-icon ${this.$t(`${key}.ICON`)}"></i>
|
||||
|
||||
@@ -40,10 +40,10 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import adminMixin from '../../../mixins/isAdmin';
|
||||
import { frontendURL } from '../../../helper/URLHelper';
|
||||
import accountMixin from '../../../mixins/account';
|
||||
|
||||
export default {
|
||||
mixins: [adminMixin],
|
||||
mixins: [accountMixin, adminMixin],
|
||||
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -60,7 +60,7 @@ export default {
|
||||
return this.$t('CONVERSATION.LOADING_CONVERSATIONS');
|
||||
},
|
||||
newInboxURL() {
|
||||
return frontendURL('settings/inboxes/new');
|
||||
return this.addAccountScoping('settings/inboxes/new');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
<template>
|
||||
<li v-if="data.attachment || data.content" :class="alignBubble">
|
||||
<li v-if="hasAttachments || data.content" :class="alignBubble">
|
||||
<div :class="wrapClass">
|
||||
<p v-tooltip.top-start="sentByMessage" :class="bubbleClass">
|
||||
<bubble-image
|
||||
v-if="data.attachment && data.attachment.file_type === 'image'"
|
||||
:url="data.attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-file
|
||||
v-if="data.attachment && data.attachment.file_type !== 'image'"
|
||||
:url="data.attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-text
|
||||
v-if="data.content"
|
||||
:message="message"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<span v-if="hasAttachments">
|
||||
<span v-for="attachment in data.attachments" :key="attachment.id">
|
||||
<bubble-image
|
||||
v-if="attachment.file_type === 'image'"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
<bubble-file
|
||||
v-if="attachment.file_type !== 'image'"
|
||||
:url="attachment.data_url"
|
||||
:readable-time="readableTime"
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isPrivate"
|
||||
v-tooltip.top-start="toolTipMessage"
|
||||
@@ -71,10 +75,16 @@ export default {
|
||||
isBubble() {
|
||||
return [0, 1, 3].includes(this.data.message_type);
|
||||
},
|
||||
hasAttachments() {
|
||||
return !!(this.data.attachments && this.data.attachments.length > 0);
|
||||
},
|
||||
hasImageAttachment() {
|
||||
const { attachment = {} } = this.data;
|
||||
const { file_type: fileType } = attachment;
|
||||
return fileType === 'image';
|
||||
if (this.hasAttachments && this.data.attachments.length > 0) {
|
||||
const { attachments = [{}] } = this.data;
|
||||
const { file_type: fileType } = attachments[0];
|
||||
return fileType === 'image';
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isPrivate() {
|
||||
return this.data.private;
|
||||
|
||||
@@ -27,10 +27,22 @@
|
||||
:data="message"
|
||||
/>
|
||||
</ul>
|
||||
<ReplyBox
|
||||
:conversation-id="currentChat.id"
|
||||
@scrollToMessage="focusLastMessage"
|
||||
/>
|
||||
<div class="conversation-footer">
|
||||
<div v-if="isAnyoneTyping" class="typing-indicator-wrap">
|
||||
<div class="typing-indicator">
|
||||
{{ typingUserNames }}
|
||||
<img
|
||||
class="gif"
|
||||
src="~dashboard/assets/images/typing.gif"
|
||||
alt="Someone is typing"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ReplyBox
|
||||
:conversation-id="currentChat.id"
|
||||
@scrollToMessage="focusLastMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -42,6 +54,7 @@ import ConversationHeader from './ConversationHeader';
|
||||
import ReplyBox from './ReplyBox';
|
||||
import Message from './Message';
|
||||
import conversationMixin from '../../../mixins/conversations';
|
||||
import { getTypingUsersText } from '../../../helper/commons';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@@ -81,6 +94,27 @@ export default {
|
||||
loadingChatList: 'getChatListLoadingStatus',
|
||||
}),
|
||||
|
||||
typingUsersList() {
|
||||
const userList = this.$store.getters[
|
||||
'conversationTypingStatus/getUserList'
|
||||
](this.currentChat.id);
|
||||
return userList;
|
||||
},
|
||||
isAnyoneTyping() {
|
||||
const userList = this.typingUsersList;
|
||||
return userList.length !== 0;
|
||||
},
|
||||
typingUserNames() {
|
||||
const userList = this.typingUsersList;
|
||||
|
||||
if (this.isAnyoneTyping) {
|
||||
const userListAsName = getTypingUsersText(userList);
|
||||
return userListAsName;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
|
||||
getMessages() {
|
||||
const [chat] = this.allConversations.filter(
|
||||
c => c.id === this.currentChat.id
|
||||
|
||||
@@ -20,12 +20,13 @@
|
||||
class="input"
|
||||
type="text"
|
||||
:placeholder="$t(messagePlaceHolder())"
|
||||
@click="onClick()"
|
||||
@blur="onBlur()"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
<file-upload
|
||||
v-if="showFileUpload"
|
||||
:size="4096 * 4096"
|
||||
accept="jpg,jpeg,png,mp3,ogg,amr,pdf,mp4"
|
||||
@input-file="onFileUpload"
|
||||
>
|
||||
<i
|
||||
@@ -142,7 +143,10 @@ export default {
|
||||
return 10000;
|
||||
},
|
||||
showFileUpload() {
|
||||
return this.channelType === 'Channel::WebWidget';
|
||||
return (
|
||||
this.channelType === 'Channel::WebWidget' ||
|
||||
this.channelType === 'Channel::TwilioSms'
|
||||
);
|
||||
},
|
||||
replyButtonLabel() {
|
||||
if (this.isPrivate) {
|
||||
@@ -256,25 +260,16 @@ export default {
|
||||
onBlur() {
|
||||
this.toggleTyping('off');
|
||||
},
|
||||
onClick() {
|
||||
this.markSeen();
|
||||
onFocus() {
|
||||
this.toggleTyping('on');
|
||||
},
|
||||
markSeen() {
|
||||
if (this.channelType === 'Channel::FacebookPage') {
|
||||
this.$store.dispatch('markSeen', {
|
||||
inboxId: this.currentChat.inbox_id,
|
||||
contactId: this.currentChat.meta.sender.id,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
toggleTyping(status) {
|
||||
if (this.channelType === 'Channel::FacebookPage') {
|
||||
if (this.channelType === 'Channel::WebWidget' && !this.isPrivate) {
|
||||
const conversationId = this.currentChat.id;
|
||||
this.$store.dispatch('toggleTyping', {
|
||||
status,
|
||||
inboxId: this.currentChat.inbox_id,
|
||||
contactId: this.currentChat.meta.sender.id,
|
||||
conversationId,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -295,6 +290,9 @@ export default {
|
||||
},
|
||||
|
||||
onFileUpload(file) {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
this.isUploading.image = true;
|
||||
this.$store
|
||||
.dispatch('sendAttachment', [this.currentChat.id, { file: file.file }])
|
||||
|
||||
@@ -44,12 +44,12 @@ export default {
|
||||
.file {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: $space-normal;
|
||||
padding: $space-smaller 0;
|
||||
cursor: pointer;
|
||||
|
||||
.icon-wrap {
|
||||
font-size: $font-size-giga;
|
||||
color: $color-woot;
|
||||
color: $color-white;
|
||||
line-height: 1;
|
||||
margin-left: $space-smaller;
|
||||
margin-right: $space-slab;
|
||||
@@ -57,15 +57,22 @@ export default {
|
||||
|
||||
.text-block-title {
|
||||
margin: 0;
|
||||
color: $color-white;
|
||||
font-weight: $font-weight-bold;
|
||||
}
|
||||
|
||||
.button {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: $color-primary-light;
|
||||
}
|
||||
|
||||
.meta {
|
||||
padding-right: $space-two;
|
||||
}
|
||||
|
||||
.time {
|
||||
min-width: $space-larger;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<span class="message-text__wrap">
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
<span v-html="message"></span>
|
||||
<span class="time">{{ readableTime }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -3,37 +3,39 @@
|
||||
<header class="emoji-dialog-header" role="menu">
|
||||
<ul>
|
||||
<li
|
||||
v-bind:class="{ 'active': selectedKey === category.key }"
|
||||
v-for="category in categoryList"
|
||||
:key="category.key"
|
||||
:class="{ active: selectedKey === category.key }"
|
||||
@click="changeCategory(category)"
|
||||
>
|
||||
<div
|
||||
@click="changeCategory(category)"
|
||||
role="menuitem"
|
||||
class="emojione"
|
||||
v-html="getEmojiUnicode(`:${category.emoji}:`)"
|
||||
>
|
||||
</div>
|
||||
@click="changeCategory(category)"
|
||||
v-html="` ${getEmojiUnicode(`:${category.emoji}:`)}`"
|
||||
></div>
|
||||
</li>
|
||||
</ul>
|
||||
</header>
|
||||
<div class="emoji-row">
|
||||
<h5 class="emoji-category-title">{{selectedKey}}</h5>
|
||||
<h5 class="emoji-category-title">
|
||||
{{ selectedKey }}
|
||||
</h5>
|
||||
<div
|
||||
v-for="(emoji, key) in selectedEmojis"
|
||||
v-for="emoji in filteredSelectedEmojis"
|
||||
:key="emoji.shortname"
|
||||
role="menuitem"
|
||||
:class="`emojione`"
|
||||
v-html="getEmojiUnicode(emoji[emoji.length - 1].shortname)"
|
||||
v-if="filterEmoji(emoji[emoji.length - 1].shortname)"
|
||||
class="emojione"
|
||||
track-by="$index"
|
||||
@click="onClick(emoji[emoji.length - 1])"
|
||||
@click="onClick(emoji)"
|
||||
v-html="getEmojiUnicode(emoji.shortname)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import strategy from 'emojione/emoji.json';
|
||||
import categoryList from './categories';
|
||||
import { getEmojiUnicode } from './utils';
|
||||
@@ -44,7 +46,7 @@ export default {
|
||||
return {
|
||||
selectedKey: 'people',
|
||||
categoryList,
|
||||
selectedEmojis: [],
|
||||
selectedEmojis: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -76,13 +78,29 @@ export default {
|
||||
}
|
||||
return emojiArr;
|
||||
},
|
||||
filteredSelectedEmojis() {
|
||||
const emojis = this.selectedEmojis;
|
||||
const filteredEmojis = Object.keys(emojis)
|
||||
.map(key => {
|
||||
const emoji = emojis[key];
|
||||
const [lastEmoji] = emoji.slice(-1);
|
||||
return { ...lastEmoji, key };
|
||||
})
|
||||
.filter(emoji => {
|
||||
const { shortname } = emoji;
|
||||
if (shortname) {
|
||||
return this.filterEmoji(shortname);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return filteredEmojis;
|
||||
},
|
||||
},
|
||||
// On mount render initial emoji
|
||||
mounted() {
|
||||
this.getInitialEmoji();
|
||||
},
|
||||
methods: {
|
||||
|
||||
// Change category and associated emojis
|
||||
changeCategory(category) {
|
||||
this.selectedKey = category.key;
|
||||
@@ -101,3 +119,6 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import '~dashboard/assets/scss/widgets/emojiinput';
|
||||
</style>
|
||||
|
||||
@@ -4,16 +4,25 @@ import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnec
|
||||
class ActionCableConnector extends BaseActionCableConnector {
|
||||
constructor(app, pubsubToken) {
|
||||
super(app, pubsubToken);
|
||||
this.CancelTyping = [];
|
||||
this.events = {
|
||||
'message.created': this.onMessageCreated,
|
||||
'message.updated': this.onMessageUpdated,
|
||||
'conversation.created': this.onConversationCreated,
|
||||
'status_change:conversation': this.onStatusChange,
|
||||
'conversation.opened': this.onStatusChange,
|
||||
'conversation.resolved': this.onStatusChange,
|
||||
'user:logout': this.onLogout,
|
||||
'page:reload': this.onReload,
|
||||
'assignee.changed': this.onAssigneeChanged,
|
||||
'conversation.typing_on': this.onTypingOn,
|
||||
'conversation.typing_off': this.onTypingOff,
|
||||
};
|
||||
}
|
||||
|
||||
onMessageUpdated = data => {
|
||||
this.app.$store.dispatch('updateMessage', data);
|
||||
};
|
||||
|
||||
onAssigneeChanged = payload => {
|
||||
const { meta = {}, id } = payload;
|
||||
const { assignee } = meta || {};
|
||||
@@ -35,7 +44,45 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
onReload = () => window.location.reload();
|
||||
|
||||
onStatusChange = data => {
|
||||
this.app.$store.dispatch('addConversation', data);
|
||||
this.app.$store.dispatch('updateConversation', data);
|
||||
};
|
||||
|
||||
onTypingOn = ({ conversation, user }) => {
|
||||
const conversationId = conversation.id;
|
||||
|
||||
this.clearTimer(conversationId);
|
||||
this.app.$store.dispatch('conversationTypingStatus/create', {
|
||||
conversationId,
|
||||
user,
|
||||
});
|
||||
this.initTimer({ conversation, user });
|
||||
};
|
||||
|
||||
onTypingOff = ({ conversation, user }) => {
|
||||
const conversationId = conversation.id;
|
||||
|
||||
this.clearTimer(conversationId);
|
||||
this.app.$store.dispatch('conversationTypingStatus/destroy', {
|
||||
conversationId,
|
||||
user,
|
||||
});
|
||||
};
|
||||
|
||||
clearTimer = conversationId => {
|
||||
const timerEvent = this.CancelTyping[conversationId];
|
||||
|
||||
if (timerEvent) {
|
||||
clearTimeout(timerEvent);
|
||||
this.CancelTyping[conversationId] = null;
|
||||
}
|
||||
};
|
||||
|
||||
initTimer = ({ conversation, user }) => {
|
||||
const conversationId = conversation.id;
|
||||
// Turn off typing automatically after 30 seconds
|
||||
this.CancelTyping[conversationId] = setTimeout(() => {
|
||||
this.onTypingOff({ conversation, user });
|
||||
}, 30000);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,3 +9,20 @@ export default () => {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getTypingUsersText = (users = []) => {
|
||||
const count = users.length;
|
||||
if (count === 1) {
|
||||
const [user] = users;
|
||||
return `${user.name} is typing`;
|
||||
}
|
||||
|
||||
if (count === 2) {
|
||||
const [first, second] = users;
|
||||
return `${first.name} and ${second.name} are typing`;
|
||||
}
|
||||
|
||||
const [user] = users;
|
||||
const rest = users.length - 1;
|
||||
return `${user.name} and ${rest} others are typing`;
|
||||
};
|
||||
|
||||
93
app/javascript/dashboard/helper/pushHelper.js
Normal file
93
app/javascript/dashboard/helper/pushHelper.js
Normal file
@@ -0,0 +1,93 @@
|
||||
/* eslint-disable no-console */
|
||||
import NotificationSubscriptions from '../api/notificationSubscription';
|
||||
import auth from '../api/auth';
|
||||
|
||||
export const verifyServiceWorkerExistence = (callback = () => {}) => {
|
||||
if (!('serviceWorker' in navigator)) {
|
||||
// Service Worker isn't supported on this browser, disable or hide UI.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('PushManager' in window)) {
|
||||
// Push isn't supported on this browser, disable or hide UI.
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.serviceWorker
|
||||
.register('/sw.js')
|
||||
.then(registration => callback(registration))
|
||||
.catch(registrationError => {
|
||||
// eslint-disable-next-line
|
||||
console.log('SW registration failed: ', registrationError);
|
||||
});
|
||||
};
|
||||
|
||||
export const hasPushPermissions = () => {
|
||||
if ('Notification' in window) {
|
||||
return Notification.permission === 'granted';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const generateKeys = str =>
|
||||
btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
export const getPushSubscriptionPayload = subscription => ({
|
||||
subscription_type: 'browser_push',
|
||||
subscription_attributes: {
|
||||
endpoint: subscription.endpoint,
|
||||
p256dh: generateKeys(subscription.getKey('p256dh')),
|
||||
auth: generateKeys(subscription.getKey('auth')),
|
||||
},
|
||||
});
|
||||
|
||||
export const sendRegistrationToServer = subscription => {
|
||||
if (auth.isLoggedIn()) {
|
||||
return NotificationSubscriptions.create(
|
||||
getPushSubscriptionPayload(subscription)
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const registerSubscription = (onSuccess = () => {}) => {
|
||||
if (!window.chatwootConfig.vapidPublicKey) {
|
||||
return;
|
||||
}
|
||||
navigator.serviceWorker.ready
|
||||
.then(serviceWorkerRegistration =>
|
||||
serviceWorkerRegistration.pushManager.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: window.chatwootConfig.vapidPublicKey,
|
||||
})
|
||||
)
|
||||
.then(sendRegistrationToServer)
|
||||
.then(() => {
|
||||
onSuccess();
|
||||
})
|
||||
.catch(() => {
|
||||
window.bus.$emit(
|
||||
'newToastMessage',
|
||||
'This browser does not support desktop notification'
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const requestPushPermissions = ({ onSuccess }) => {
|
||||
if (!('Notification' in window)) {
|
||||
window.bus.$emit(
|
||||
'newToastMessage',
|
||||
'This browser does not support desktop notification'
|
||||
);
|
||||
} else if (Notification.permission === 'granted') {
|
||||
registerSubscription(onSuccess);
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission(permission => {
|
||||
if (permission === 'granted') {
|
||||
registerSubscription(onSuccess);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
26
app/javascript/dashboard/helper/specs/commons.spec.js
Normal file
26
app/javascript/dashboard/helper/specs/commons.spec.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { getTypingUsersText } from '../commons';
|
||||
|
||||
describe('#getTypingUsersText', () => {
|
||||
it('returns the correct text is there is only one typing user', () => {
|
||||
expect(getTypingUsersText([{ name: 'Pranav' }])).toEqual(
|
||||
'Pranav is typing'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the correct text is there are two typing users', () => {
|
||||
expect(
|
||||
getTypingUsersText([{ name: 'Pranav' }, { name: 'Nithin' }])
|
||||
).toEqual('Pranav and Nithin are typing');
|
||||
});
|
||||
|
||||
it('returns the correct text is there are more than two users are typing', () => {
|
||||
expect(
|
||||
getTypingUsersText([
|
||||
{ name: 'Pranav' },
|
||||
{ name: 'Nithin' },
|
||||
{ name: 'Subin' },
|
||||
{ name: 'Sojan' },
|
||||
])
|
||||
).toEqual('Pranav and 3 others are typing');
|
||||
});
|
||||
});
|
||||
@@ -1,37 +0,0 @@
|
||||
import de from './locale/de';
|
||||
|
||||
export default {
|
||||
...de,
|
||||
APP_GLOBAL: {
|
||||
TRIAL_MESSAGE: 'verbleibende Tage Probezeit.',
|
||||
TRAIL_BUTTON: 'Kaufe jetzt',
|
||||
},
|
||||
COMPONENTS: {
|
||||
CODE: {
|
||||
BUTTON_TEXT: 'Kopieren',
|
||||
COPY_SUCCESSFUL: 'Code erfolgreich in die Zwischenablage kopiert',
|
||||
},
|
||||
FILE_BUBBLE: {
|
||||
DOWNLOAD: 'Herunterladen',
|
||||
UPLOADING: 'Hochladen...',
|
||||
},
|
||||
},
|
||||
CONFIRM_EMAIL: 'Überprüfen...',
|
||||
SETTINGS: {
|
||||
INBOXES: {
|
||||
NEW_INBOX: 'Posteingang hinzufügen',
|
||||
},
|
||||
},
|
||||
SIDEBAR: {
|
||||
CONVERSATIONS: 'Gespräche',
|
||||
REPORTS: 'Berichte',
|
||||
SETTINGS: 'Die Einstellungen',
|
||||
HOME: 'Zuhause',
|
||||
AGENTS: 'Agenten',
|
||||
INBOXES: 'Posteingänge',
|
||||
CANNED_RESPONSES: 'Vorgefertigte Antworten',
|
||||
BILLING: 'Abrechnung',
|
||||
INTEGRATIONS: 'Integrationen',
|
||||
ACCOUNT_SETTINGS: 'Kontoeinstellungen',
|
||||
},
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import en from './locale/en';
|
||||
|
||||
export default {
|
||||
...en,
|
||||
APP_GLOBAL: {
|
||||
TRIAL_MESSAGE: 'days trial remaining.',
|
||||
TRAIL_BUTTON: 'Buy Now',
|
||||
},
|
||||
COMPONENTS: {
|
||||
CODE: {
|
||||
BUTTON_TEXT: 'Copy',
|
||||
COPY_SUCCESSFUL: 'Code copied to clipboard successfully',
|
||||
},
|
||||
FILE_BUBBLE: {
|
||||
DOWNLOAD: 'Download',
|
||||
UPLOADING: 'Uploading...',
|
||||
},
|
||||
},
|
||||
CONFIRM_EMAIL: 'Verifying...',
|
||||
SETTINGS: {
|
||||
INBOXES: {
|
||||
NEW_INBOX: 'Add Inbox',
|
||||
},
|
||||
},
|
||||
SIDEBAR: {
|
||||
CONVERSATIONS: 'Conversations',
|
||||
REPORTS: 'Reports',
|
||||
SETTINGS: 'Settings',
|
||||
HOME: 'Home',
|
||||
AGENTS: 'Agents',
|
||||
INBOXES: 'Inboxes',
|
||||
CANNED_RESPONSES: 'Canned Responses',
|
||||
BILLING: 'Billing',
|
||||
INTEGRATIONS: 'Integrations',
|
||||
ACCOUNT_SETTINGS: 'Account Settings',
|
||||
},
|
||||
};
|
||||
@@ -1,7 +1,21 @@
|
||||
import en from './en';
|
||||
import de from './de';
|
||||
import ca from './locale/ca';
|
||||
import ro from './locale/ro';
|
||||
import fr from './locale/fr';
|
||||
import pt_BR from './locale/pt_BR';
|
||||
import de from './locale/de';
|
||||
import el from './locale/el';
|
||||
import en from './locale/en';
|
||||
import ml from './locale/ml';
|
||||
import pt from './locale/pt';
|
||||
|
||||
export default {
|
||||
ca,
|
||||
de,
|
||||
el,
|
||||
en,
|
||||
fr,
|
||||
ml,
|
||||
pt_BR,
|
||||
pt,
|
||||
ro,
|
||||
};
|
||||
|
||||
101
app/javascript/dashboard/i18n/locale/ca/agentMgmt.json
Normal file
101
app/javascript/dashboard/i18n/locale/ca/agentMgmt.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"AGENT_MGMT": {
|
||||
"HEADER": "Agents",
|
||||
"HEADER_BTN_TXT": "Afegir Agent",
|
||||
"LOADING": "S'està recollint la llista d'agents",
|
||||
"SIDEBAR_TXT": "<p><b>Agents</b></p> <p> Un <b>Agent</b> és un membre del teu equip de suport al client. </p><p> Els agents podran veure i respondre als missatges dels vostres usuaris. La llista mostra tots els agents que hi ha actualment al vostre compte. </p><p> Clica en <b>Afegir Agent</b> per afegir un nou agent. L’agent que afegiu rebrà un correu electrònic amb un enllaç de confirmació per activar el seu compte, després del qual podran accedir a Chatwoot i respondre als missatges. </p><p> L’accés a les funcions de Chatwoot es basa en els següents rols. </p><p> <b>Agent</b> - els agents amb aquest rol només poden accedir a bústies de sortida, informes i converses. </p><p> <b>Administrador/a</b> - L’administrador/a tindrà accés a totes les funcions de Chatwoot habilitades per al vostre compte, incloses les configuracions i la facturació, juntament amb tots els privilegis dels agents normals.</p>",
|
||||
"AGENT_TYPES": [
|
||||
{
|
||||
"name": "administrador/a",
|
||||
"label": "Administrador/a"
|
||||
},
|
||||
{
|
||||
"name": "agent",
|
||||
"label": "Agent"
|
||||
}
|
||||
],
|
||||
"LIST": {
|
||||
"404": "No hi ha agents associats a aquest compte",
|
||||
"TITLE": "Gestiona agents en el teu equip",
|
||||
"DESC": "Pots afegir/esborrar agents del teu equip.",
|
||||
"NAME": "Nom",
|
||||
"EMAIL": "Correu electrònic",
|
||||
"STATUS": "Estat",
|
||||
"ACTIONS": "Accions",
|
||||
"VERIFIED": "Verificat",
|
||||
"VERIFICATION_PENDING": "Verificació pendent"
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Afegir agent al teu equip",
|
||||
"DESC": "Podeu afegir persones que podran gestionar suport per a les vostres safates d'entrada.",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Nom de l'Agent",
|
||||
"PLACEHOLDER": "Introduïu el nom de l'agent"
|
||||
},
|
||||
"AGENT_TYPE": {
|
||||
"LABEL": "Tipus d'Agent",
|
||||
"PLACEHOLDER": "Selecciona un tipus",
|
||||
"ERROR": "El tipus d'Agent és necessari"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Adreça de correu electrònic",
|
||||
"PLACEHOLDER": "Introduïu l'adreça de correu electrònic de l'agent"
|
||||
},
|
||||
"SUBMIT": "Afegir Agent"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Agent afegit correctament",
|
||||
"EXIST_MESSAGE": "L'adreça de correu electrònic de l'agent ja està en ús. Introduïu una altre adreça",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Esborrar",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Agent esborrat correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma l'esborrat",
|
||||
"MESSAGE": "N'estas segur? ",
|
||||
"YES": "Si, esborra ",
|
||||
"NO": "No, manten-la "
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edita l'agent",
|
||||
"FORM": {
|
||||
"NAME": {
|
||||
"LABEL": "Nom de l'Agent",
|
||||
"PLACEHOLDER": "Introduïu el nom de l'agent"
|
||||
},
|
||||
"AGENT_TYPE": {
|
||||
"LABEL": "Tipus d'Agent",
|
||||
"PLACEHOLDER": "Selecciona un tipus",
|
||||
"ERROR": "El tipus d'Agent és necessari"
|
||||
},
|
||||
"EMAIL": {
|
||||
"LABEL": "Adreça de correu electrònic",
|
||||
"PLACEHOLDER": "Introduïu l'adreça de correu electrònic de l'agent"
|
||||
},
|
||||
"SUBMIT": "Editar l'agent"
|
||||
},
|
||||
"BUTTON_TEXT": "Edita",
|
||||
"CANCEL_BUTTON_TEXT": "Cancel·la",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Agent actualitzat correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
},
|
||||
"PASSWORD_RESET": {
|
||||
"ADMIN_RESET_BUTTON": "Reinicialització de la contrasenya",
|
||||
"ADMIN_SUCCESS_MESSAGE": "S'ha enviat a l'agent un correu electrònic amb instruccions per restablir la contrasenya",
|
||||
"SUCCESS_MESSAGE": "La contrasenya de l'agent s'ha restablit correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
}
|
||||
},
|
||||
"SEARCH": {
|
||||
"NO_RESULTS": "No s'han trobat agents."
|
||||
}
|
||||
}
|
||||
}
|
||||
19
app/javascript/dashboard/i18n/locale/ca/billing.json
Normal file
19
app/javascript/dashboard/i18n/locale/ca/billing.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"BILLING": {
|
||||
"HEADER": "Facturació",
|
||||
"LOADING": "S'estan obtenin les suscripcions",
|
||||
"ACCOUNT_STATE": "Estat del compte",
|
||||
"AGENT_COUNT": "Compte d'agent",
|
||||
"PER_AGENT_COST": "Per cost d'agent",
|
||||
"TOTAL_COST": "Cost total",
|
||||
"BUTTON": {
|
||||
"ADD": "Afegir mètode de pagament",
|
||||
"EDIT": "EDITAR Mètode de pagament"
|
||||
},
|
||||
"TRIAL": {
|
||||
"TITLE": "S'ha acabat el període de prova",
|
||||
"MESSAGE": "Afegiu un mètode de pagament per continuar utilitzant Chatwoot."
|
||||
},
|
||||
"ACCOUNT_LOCKED": "El seu compte no està disponible de moment. <br>Poseu-vos en contacte amb l'administrador per reactivar-lo."
|
||||
}
|
||||
}
|
||||
74
app/javascript/dashboard/i18n/locale/ca/cannedMgmt.json
Normal file
74
app/javascript/dashboard/i18n/locale/ca/cannedMgmt.json
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"CANNED_MGMT": {
|
||||
"HEADER": "Respostes predeterminades",
|
||||
"HEADER_BTN_TXT": "Afegeix una resposta predeterminada",
|
||||
"LOADING": "S'estan recollint les respostes predeterminades",
|
||||
"SEARCH_404": "No hi ha cap resposta que coincideixi amb aquesta consulta",
|
||||
"SIDEBAR_TXT": "<p><b>Respostes predeterminades</b> </p><p> Les respostes predeterminades són plantilles de resposta que es poden utilitzar per enviar ràpidament una resposta a una conversa .</p><p> Per crear una Resposta Predeterminada, clica en <b>Afegir Resposta Predeterminada</b>. També pots editar o suprimir una resposta predeterminada fent clic al botó Edita o Suprimeix </p><p> Les respostes predeterminades s'utilitzen amb l'ajuda dels <b>Codi curt</b>. Els agents poden accedir a les respostes predeterminades en un xat escrivint <b>'/'</b> seguit del codi curt. </p>",
|
||||
"LIST": {
|
||||
"404": "No hi ha respostes predeterminades disponibles en aquest compte.",
|
||||
"TITLE": "Gestiona les respostes predeterminades",
|
||||
"DESC": "Les respostes predeterminades són plantilles de resposta predefinides que es poden utilitzar per enviar ràpidament respostes a les converses.",
|
||||
"TABLE_HEADER": [
|
||||
"Codi curt",
|
||||
"Contingut",
|
||||
"Accions"
|
||||
]
|
||||
},
|
||||
"ADD": {
|
||||
"TITLE": "Afegeix Resposta Predeterminada",
|
||||
"DESC": "Les respostes predeterminades són plantilles de resposta que es poden utilitzar per enviar ràpidament les respostes a les converses.",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "Codi curt",
|
||||
"PLACEHOLDER": "Introduïu un codi curt",
|
||||
"ERROR": "És necessari el codi curt"
|
||||
},
|
||||
"CONTENT": {
|
||||
"LABEL": "Contingut",
|
||||
"PLACEHOLDER": "Introduïu un contingut",
|
||||
"ERROR": "És necessari un contingut"
|
||||
},
|
||||
"SUBMIT": "Envia"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Resposta predeterminada afegida correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
}
|
||||
},
|
||||
"EDIT": {
|
||||
"TITLE": "Edita la resposta predeterminada",
|
||||
"FORM": {
|
||||
"SHORT_CODE": {
|
||||
"LABEL": "Codi curt",
|
||||
"PLACEHOLDER": "Introduïu un codi curt",
|
||||
"ERROR": "És necessari el codi curt"
|
||||
},
|
||||
"CONTENT": {
|
||||
"LABEL": "Contingut",
|
||||
"PLACEHOLDER": "Introduïu un contingut",
|
||||
"ERROR": "És necessari un contingut"
|
||||
},
|
||||
"SUBMIT": "Envia"
|
||||
},
|
||||
"BUTTON_TEXT": "Edita",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Resposta predeterminada actualitzada correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Esborra",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Resposta predeterminada eliminada correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma esborrat",
|
||||
"MESSAGE": "N'estas segur ",
|
||||
"YES": "Si, esborra ",
|
||||
"NO": "No, mantén-la "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
77
app/javascript/dashboard/i18n/locale/ca/chatlist.json
Normal file
77
app/javascript/dashboard/i18n/locale/ca/chatlist.json
Normal file
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"CHAT_LIST": {
|
||||
"LOADING": "S'estan obtenint converses",
|
||||
"LOAD_MORE_CONVERSATIONS": "Carrega més converses",
|
||||
"EOF": "Totes les converses carregades 🎉",
|
||||
"LIST": {
|
||||
"404": "No hi ha converses actives en aquest grup."
|
||||
},
|
||||
"TAB_HEADING": "Converses",
|
||||
"SEARCH": {
|
||||
"INPUT": "Cerca persones, xats, respostes desades .."
|
||||
},
|
||||
"STATUS_TABS": [
|
||||
{
|
||||
"NAME": "Obrir",
|
||||
"KEY": "openCount"
|
||||
},
|
||||
{
|
||||
"NAME": "Resoltes",
|
||||
"KEY": "allConvCount"
|
||||
}
|
||||
],
|
||||
"ASSIGNEE_TYPE_TABS": [
|
||||
{
|
||||
"NAME": "Meves",
|
||||
"KEY": "me",
|
||||
"COUNT_KEY": "mineCount"
|
||||
},
|
||||
{
|
||||
"NAME": "Sense assignar",
|
||||
"KEY": "unassigned",
|
||||
"COUNT_KEY": "unAssignedCount"
|
||||
},
|
||||
{
|
||||
"NAME": "Totes",
|
||||
"KEY": "all",
|
||||
"COUNT_KEY": "allCount"
|
||||
}
|
||||
],
|
||||
"CHAT_STATUS_ITEMS": [
|
||||
{
|
||||
"TEXT": "Obertes",
|
||||
"VALUE": "open"
|
||||
},
|
||||
{
|
||||
"TEXT": "Resoltes",
|
||||
"VALUE": "resolved"
|
||||
}
|
||||
],
|
||||
"ATTACHMENTS": {
|
||||
"image": {
|
||||
"ICON": "ion-image",
|
||||
"CONTENT": "Missatge d'imatge"
|
||||
},
|
||||
"audio": {
|
||||
"ICON": "ion-volume-high",
|
||||
"CONTENT": "Missatge d'àudio"
|
||||
},
|
||||
"video": {
|
||||
"ICON": "ion-ios-videocam",
|
||||
"CONTENT": "Missatge de vídeo"
|
||||
},
|
||||
"file": {
|
||||
"ICON": "ion-document",
|
||||
"CONTENT": "Fitxer adjunt"
|
||||
},
|
||||
"location": {
|
||||
"ICON": "ion-ios-location",
|
||||
"CONTENT": "Ubicació"
|
||||
},
|
||||
"fallback": {
|
||||
"ICON": "ion-link",
|
||||
"CONTENT": "ha compartit una URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
app/javascript/dashboard/i18n/locale/ca/contact.json
Normal file
20
app/javascript/dashboard/i18n/locale/ca/contact.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"CONTACT_PANEL": {
|
||||
"CONVERSATION_TITLE": "Detalls de les converses",
|
||||
"BROWSER": "Navegador",
|
||||
"OS": "Sistema operatiu",
|
||||
"INITIATED_FROM": "Iniciada des de",
|
||||
"INITIATED_AT": "Iniciada a les",
|
||||
"CONVERSATIONS": {
|
||||
"NO_RECORDS_FOUND": "No hi han converses prèvies associades a aquest contacte.",
|
||||
"TITLE": "Converses prèvies"
|
||||
},
|
||||
"LABELS": {
|
||||
"TITLE": "Etiquetes de converses",
|
||||
"UPDATE_BUTTON": "Actualitza etiquetes",
|
||||
"UPDATE_ERROR": "No s'han pogut actualitzar les etiquetes, torna-ho a provar.",
|
||||
"TAG_PLACEHOLDER": "Afegeix una etiqueta nova",
|
||||
"PLACEHOLDER": "Cerca o afegeix una etiqueta"
|
||||
}
|
||||
}
|
||||
}
|
||||
35
app/javascript/dashboard/i18n/locale/ca/conversation.json
Normal file
35
app/javascript/dashboard/i18n/locale/ca/conversation.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"CONVERSATION": {
|
||||
"404": "Si us plau, selecciona una conversa al panell de l’esquerra",
|
||||
"NO_MESSAGE_1": "Uh oh! Sembla que no hi ha missatges de clients a la safata d'entrada.",
|
||||
"NO_MESSAGE_2": " per enviar un missatge a la vostra pàgina!",
|
||||
"NO_INBOX_1": "Hola! Sembla que encara no heu afegit cap safata d'entrada.",
|
||||
"NO_INBOX_2": " per començar",
|
||||
"NO_INBOX_AGENT": "Uh Oh! Sembla que no ets a cap safata d'entrada. Si us plau, poseu-vos en contacte amb l'administrador",
|
||||
"CLICK_HERE": "Clica aquí",
|
||||
"LOADING_INBOXES": "S'estan carregant les safates d'entrada",
|
||||
"LOADING_CONVERSATIONS": "S'estan carregant les converses",
|
||||
"DOWNLOAD": "Descarrega",
|
||||
"HEADER": {
|
||||
"RESOLVE_ACTION": "Resoldre",
|
||||
"REOPEN_ACTION": "Tornar a obrir",
|
||||
"OPEN": "Més",
|
||||
"CLOSE": "Tanca",
|
||||
"DETAILS": "detalls"
|
||||
},
|
||||
"FOOTER": {
|
||||
"MSG_INPUT": "Shift + enter per a una línia nova. Comença amb '/' per seleccionar una resposta predeterminada.",
|
||||
"PRIVATE_MSG_INPUT": "Shift + enter per una línia nova. Això serà visible només per als Agents"
|
||||
},
|
||||
"REPLYBOX": {
|
||||
"REPLY": "Respon",
|
||||
"PRIVATE_NOTE": "Nota privada",
|
||||
"SEND": "Envia",
|
||||
"CREATE": "Afegeix una nota",
|
||||
"TWEET": "Tuit"
|
||||
},
|
||||
"VISIBLE_TO_AGENTS": "Nota privada: Només és visible per tu i el vostre equip",
|
||||
"CHANGE_STATUS": "Estat de la conversa canviat",
|
||||
"CHANGE_AGENT": "Assignació de la conversa canviat"
|
||||
}
|
||||
}
|
||||
27
app/javascript/dashboard/i18n/locale/ca/generalSettings.json
Normal file
27
app/javascript/dashboard/i18n/locale/ca/generalSettings.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"GENERAL_SETTINGS": {
|
||||
"TITLE": "Configuració del compte",
|
||||
"SUBMIT": "Actualització de la configuració",
|
||||
"UPDATE": {
|
||||
"ERROR": "No s'ha pogut actualitzar la configuració, torna-ho a provar!",
|
||||
"SUCCESS": "La configuració del compte s'ha actualitzat correctament"
|
||||
},
|
||||
"FORM": {
|
||||
"ERROR": "Corregiu els errors del formulari",
|
||||
"GENERAL_SECTION": {
|
||||
"TITLE": "Configuració general",
|
||||
"NOTE": ""
|
||||
},
|
||||
"NAME": {
|
||||
"LABEL": "Nom del compte",
|
||||
"PLACEHOLDER": "El nom del vostre compte",
|
||||
"ERROR": "Introduïu un nom de compte vàlid"
|
||||
},
|
||||
"LANGUAGE": {
|
||||
"LABEL": "Idioma del lloc (Beta)",
|
||||
"PLACEHOLDER": "El nom del vostre compte",
|
||||
"ERROR": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
138
app/javascript/dashboard/i18n/locale/ca/inboxMgmt.json
Normal file
138
app/javascript/dashboard/i18n/locale/ca/inboxMgmt.json
Normal file
@@ -0,0 +1,138 @@
|
||||
{
|
||||
"INBOX_MGMT": {
|
||||
"HEADER": "Safates d'entrada",
|
||||
"SIDEBAR_TXT": "<p><b>Safata d'entrada</b></p> <p> Quan connecteu un lloc web o una pàgina de facebook a Chatwoot, es diu <b>Safata d'entrada</b>. Teniu bústies d'entrada il·limitades al vostre compte de Chatwoot. </p><p> Feu click a <b>Afegir safata d'entrada</b> per connectar-vos a un lloc web o a una pàgina de Facebook. </p><p> Al <a href=\"/app/dashboard\">Tauler de control</a>, pots veure totes les converses de totes les teves safates d'entrada en un sol lloc i respondre-les a la pestanya `Converses`. </p><p> També pots veure converses específiques per a una safata d’entrada si feu clic al nom de la safata d'entrada, al panell esquerre de la taula. </p>",
|
||||
"LIST": {
|
||||
"404": "No hi ha cap safata d'entrada connectat a aquest compte."
|
||||
},
|
||||
"CREATE_FLOW": [
|
||||
{ "title": "Triar canal", "route": "settings_inbox_new", "body": "Trieu el proveïdor que vulgueu integrar amb Chatwoot." },
|
||||
{ "title": "Crear safata d'entrada", "route": "settings_inboxes_page_channel", "body": "Autentiqueu el vostre compte i creeu una safata d'entrada." },
|
||||
{ "title": "Afegir agents", "route": "settings_inboxes_add_agents", "body": "Afegir agents a la safata d'entrada creada." },
|
||||
{ "title": "Voila!", "route": "settings_inbox_finish", "body": "Ja estàs preparat!" }
|
||||
],
|
||||
"ADD": {
|
||||
"FB": {
|
||||
"HELP": "PD: Al iniciar la sessió, només accediu als missatges de la vostra pàgina. Chatwoot mai no podrà accedir als vostres missatges privats."
|
||||
},
|
||||
"TWITTER": {
|
||||
"HELP": "Per afegir el teu perfil de Twitter com a canal, has d'autentificar el vostre perfil de Twitter fent clic a 'Inicieu la sessió amb Twitter' "
|
||||
},
|
||||
"WEBSITE_CHANNEL": {
|
||||
"TITLE": "Canal Web",
|
||||
"DESC": "Crea un canal per al vostre lloc web i comença a donar suport als vostres clients mitjançant el teu widget del lloc web.",
|
||||
"LOADING_MESSAGE": "S'està creant el canal de suport web",
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Nom del lloc web",
|
||||
"PLACEHOLDER": "Introduïu el nom del vostre lloc web (per exemple, Acme Inc)"
|
||||
},
|
||||
"CHANNEL_DOMAIN": {
|
||||
"LABEL": "Domini del lloc web",
|
||||
"PLACEHOLDER": "Introduïu el vostre domini de lloc web (pe: acme.com)"
|
||||
},
|
||||
"WIDGET_COLOR": {
|
||||
"LABEL": "Color del Widget",
|
||||
"PLACEHOLDER": "Actualitza el color del widget"
|
||||
},
|
||||
"SUBMIT_BUTTON":"Crea la safata entrada"
|
||||
},
|
||||
"TWILIO": {
|
||||
"TITLE": "Canal Twilio SMS",
|
||||
"DESC": "Integra Twilio i comença a donar suport als teus clients mitjançant SMS.",
|
||||
"ACCOUNT_SID": {
|
||||
"LABEL": "Compte SID",
|
||||
"PLACEHOLDER": "Introduïu el vostre compte Twilio SID",
|
||||
"ERROR": "Aquest camp és obligatori"
|
||||
},
|
||||
"AUTH_TOKEN": {
|
||||
"LABEL": "Auth Token",
|
||||
"PLACEHOLDER": "Introduïu el vostre Twilio Auth Token",
|
||||
"ERROR": "Aquest camp és obligatori"
|
||||
},
|
||||
"CHANNEL_NAME": {
|
||||
"LABEL": "Nom del canal",
|
||||
"PLACEHOLDER": "Introduïu el nom del canal",
|
||||
"ERROR": "Aquest camp és obligatori"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "Número de telèfon",
|
||||
"PLACEHOLDER": "Introduïu el número de telèfon des del qual serà enviat el missatge.",
|
||||
"ERROR": "Introduïu un valor vàlid. El número de telèfon hauria de començar amb el signe `+`."
|
||||
},
|
||||
"SUBMIT_BUTTON": "Crear un canal Twilio",
|
||||
"API": {
|
||||
"ERROR_MESSAGE": "No hem pogut autenticar les credencials de Twilio, prova de nou"
|
||||
}
|
||||
},
|
||||
"AUTH": {
|
||||
"TITLE": "Canals",
|
||||
"DESC": "Actualment estan suportats widgets de xat en directe per a llocs web, pàgines de Facebook i perfils de Twitter. Estem treballant en més plataformes com Whatsapp, correu electrònic, Telegram i Line, que estaran disponibles en breu"
|
||||
},
|
||||
"AGENTS": {
|
||||
"TITLE": "Agents",
|
||||
"DESC": "Aquí podeu afegir agents per gestionar la vostra safata d'entrada de nova creació. Només aquests agents seleccionats tindran accés a la vostra safata d'entrada. Els agents que no formen part d'aquesta safata d'entrada no podran veure ni respondre als missatges d'aquesta safata d'entrada quan s’inicien. <br><b>PD:</b> Com a administrador, si necessiteu accés a totes les bústies d’entrada, heu d’afegir-vos com a agent a totes les bústies de sortida que creeu."
|
||||
},
|
||||
"DETAILS": {
|
||||
"TITLE": "Detalls de la safata d'entrada",
|
||||
"DESC": "Des del següent menú desplegable, seleccioneu la pàgina de Facebook que voleu connectar a Chatwoot. També podeu donar un nom personalitzat a la safata d'entrada per a una millor identificació."
|
||||
},
|
||||
"FINISH":{
|
||||
"TITLE": "L'has clavat!",
|
||||
"DESC": "Heu acabat d'integrar la vostra pàgina de Facebook amb Chatwoot. La propera vegada que un client escrigui un missatge a la vostra pàgina, la conversa apareixerà automàticament a la safata d'entrada. <br>També us proporcionem un script del widget que podeu afegir fàcilment al vostre web. Una vegada que estigui operatiu al vostre web, els clients us podran enviar missatges des del web sense l’ajuda de cap eina externa i la conversa apareixerà aquí mateix, a Chatwoot. <br>Genial, eh? Bé, segur que intentem ser-ho :)"
|
||||
}
|
||||
},
|
||||
"DETAILS": {
|
||||
"LOADING_FB": "S'està autenticant amb Facebook...",
|
||||
"ERROR_FB_AUTH": "Alguna cosa ha anat malament, actualitza la pàgina ...",
|
||||
"CREATING_CHANNEL": "S'està creant la safata d'entrada...",
|
||||
"TITLE": "Configura els detalls de la safata d'entrada",
|
||||
"DESC": ""
|
||||
},
|
||||
"AGENTS": {
|
||||
"BUTTON_TEXT": "Afegir agents",
|
||||
"ADD_AGENTS": "S'estan afegint agents a la vostre safata d'entrada..."
|
||||
},
|
||||
"FINISH": {
|
||||
"TITLE": "La vostra safata d'entrada està a punt!",
|
||||
"MESSAGE": "Ja podeu interactuar amb els vostres clients a través del vostre canal nou. Feliç suport",
|
||||
"BUTTON_TEXT": "Porta'm allà",
|
||||
"WEBSITE_SUCCESS": "Heu finalitzat amb èxit la creació d'un canal web. Copieu el codi que es mostra a continuació i enganxeu-lo al lloc web. La propera vegada que un client utilitzi el xat en directe, la conversa apareixerà automàticament a la safata d'entrada."
|
||||
},
|
||||
"REAUTH": "Reautoritza",
|
||||
"VIEW": "Veure",
|
||||
"EDIT": {
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "El color del widget s'ha actualitzat correctament",
|
||||
"AUTO_ASSIGNMENT_SUCCESS_MESSAGE": "Assignació automàtica actualitzada correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut actualitzar el color del widget. Torneu-ho a provar més endavant."
|
||||
},
|
||||
"AUTO_ASSIGNMENT": {
|
||||
"ENABLED": "Habilita",
|
||||
"DISABLED": "Inhabilita"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Suprimeix",
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma esborrat",
|
||||
"MESSAGE": "N'estas segur? ",
|
||||
"YES": "Si, esborra ",
|
||||
"NO": "No, manten-la "
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "S'ha suprimit la safata d'entrada correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut eliminar la safata d'entrada. Torneu-ho a provar més endavant."
|
||||
}
|
||||
},
|
||||
"SETTINGS": "Configuracions",
|
||||
"SETTINGS_POPUP": {
|
||||
"MESSENGER_HEADING": "Script del missatger",
|
||||
"MESSENGER_SUB_HEAD": "Col·loca aquest botó dins de l'etiqueta body",
|
||||
"INBOX_AGENTS": "Agents",
|
||||
"INBOX_AGENTS_SUB_TEXT": "Afegir o eliminar agents d'aquesta safata d'entrada",
|
||||
"UPDATE": "Actualitza",
|
||||
"AUTO_ASSIGNMENT": "Activa l'assignació automàtica",
|
||||
"AUTO_ASSIGNMENT_SUB_TEXT": "Activa o desactiva l'assignació automàtica d'agents disponibles a les noves converses"
|
||||
}
|
||||
}
|
||||
}
|
||||
34
app/javascript/dashboard/i18n/locale/ca/index.js
Normal file
34
app/javascript/dashboard/i18n/locale/ca/index.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable */
|
||||
import { default as _agentMgmt } from './agentMgmt.json';
|
||||
import { default as _billing } from './billing.json';
|
||||
import { default as _cannedMgmt } from './cannedMgmt.json';
|
||||
import { default as _chatlist } from './chatlist.json';
|
||||
import { default as _contact } from './contact.json';
|
||||
import { default as _conversation } from './conversation.json';
|
||||
import { default as _inboxMgmt } from './inboxMgmt.json';
|
||||
import { default as _login } from './login.json';
|
||||
import { default as _report } from './report.json';
|
||||
import { default as _resetPassword } from './resetPassword.json';
|
||||
import { default as _setNewPassword } from './setNewPassword.json';
|
||||
import { default as _settings } from './settings.json';
|
||||
import { default as _signup } from './signup.json';
|
||||
import { default as _integrations } from './integrations.json';
|
||||
import { default as _generalSettings } from './generalSettings.json';
|
||||
|
||||
export default {
|
||||
..._agentMgmt,
|
||||
..._billing,
|
||||
..._cannedMgmt,
|
||||
..._chatlist,
|
||||
..._contact,
|
||||
..._conversation,
|
||||
..._inboxMgmt,
|
||||
..._login,
|
||||
..._report,
|
||||
..._resetPassword,
|
||||
..._setNewPassword,
|
||||
..._settings,
|
||||
..._signup,
|
||||
..._integrations,
|
||||
..._generalSettings,
|
||||
};
|
||||
54
app/javascript/dashboard/i18n/locale/ca/integrations.json
Normal file
54
app/javascript/dashboard/i18n/locale/ca/integrations.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"INTEGRATION_SETTINGS": {
|
||||
"HEADER": "Integracions",
|
||||
"WEBHOOK": {
|
||||
"TITLE": "Webhook",
|
||||
"CONFIGURE": "Configura",
|
||||
"HEADER": "Configuració Webhook",
|
||||
"HEADER_BTN_TXT": "Afegeix un nou webhook",
|
||||
"INTEGRATION_TXT": "Els esdeveniments de Webhook us proporcionen informació en temps real sobre el que passa al vostre compte de Chatwoot. Podeu utilitzar els webhooks per comunicar els esdeveniments a les vostres aplicacions preferides com Slack o Github. Feu clic a Configura per configurar els enllaços web.",
|
||||
"LOADING": "S'estan recollint els webhooks adjunts",
|
||||
"SEARCH_404": "No hi ha articles que coincideixin amb aquesta consulta",
|
||||
"SIDEBAR_TXT": "<p><b>Webhooks</b> </p> <p>Els webhooks són callbacks HTTP que es poden definir per a cada compte. Es produeixen per esdeveniments com la creació de missatges a Chatwoot. Podeu crear més d'un webhook per a aquest compte. <br /><br /> Per crear un <b>webhook</b>, feu clic al botó <b> Afegir nou webhook </b>. També podeu eliminar qualsevol webhook existent fent clic al botó Elimina.</p>",
|
||||
"LIST": {
|
||||
"404": "No hi ha cap webhooks configurat per a aquest compte.",
|
||||
"TITLE": "Gestiona els webhooks",
|
||||
"DESC": "Els webhooks són plantilles de resposta predefinides que es poden utilitzar per enviar ràpidament respostes a una conversa.",
|
||||
"TABLE_HEADER": [
|
||||
"Punt final del webhook",
|
||||
"Accions"
|
||||
]
|
||||
},
|
||||
"ADD": {
|
||||
"CANCEL": "Cancel·la",
|
||||
"TITLE": "Afegir un nou webhook",
|
||||
"DESC": "Els esdeveniments de Webhook us proporcionen informació en temps real sobre el que passa al vostre compte de Chatwoot. Introduïu una URL vàlid per configurar un callback.",
|
||||
"FORM": {
|
||||
"END_POINT": {
|
||||
"LABEL": "URL del webhook",
|
||||
"PLACEHOLDER": "Exemple: https://example/api/webhook",
|
||||
"ERROR": "Introduïu una URL vàlid"
|
||||
},
|
||||
"SUBMIT": "Crear webhook"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "S'ha afegit el Webhook correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
}
|
||||
},
|
||||
"DELETE": {
|
||||
"BUTTON_TEXT": "Suprimeix",
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "S'ha esborrat el Webhook correctament",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant"
|
||||
},
|
||||
"CONFIRM": {
|
||||
"TITLE": "Confirma l'esborrat",
|
||||
"MESSAGE": "N'estàs segur ",
|
||||
"YES": "Si, esborra ",
|
||||
"NO": "No, mantén-la "
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
21
app/javascript/dashboard/i18n/locale/ca/login.json
Normal file
21
app/javascript/dashboard/i18n/locale/ca/login.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"LOGIN": {
|
||||
"TITLE": "Entra a Chatwoot",
|
||||
"EMAIL": {
|
||||
"LABEL": "Correu electrònic",
|
||||
"PLACEHOLDER": "Correu electrònic p.e.: someone@exemple.com"
|
||||
},
|
||||
"PASSWORD": {
|
||||
"LABEL": "Contrasenya",
|
||||
"PLACEHOLDER": "Contrasenya"
|
||||
},
|
||||
"API": {
|
||||
"SUCCESS_MESSAGE": "Iniciada la sessió amb èxit",
|
||||
"ERROR_MESSAGE": "No s'ha pogut connectar amb el servidor Woot. Torna-ho a provar més endavant",
|
||||
"UNAUTH": "Nom d'usuari / contrasenya incorrecte. Torna-ho a provar-ho"
|
||||
},
|
||||
"FORGOT_PASSWORD": "Has oblidat la contrasenya?",
|
||||
"CREATE_NEW_ACCOUNT": "Crear un nou compte",
|
||||
"SUBMIT": "Inicia la sessió"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user