Compare commits

..

1 commit

Author SHA1 Message Date
Pranav Raj S
ead7a5fc87 feat: Link to a message 2022-11-09 19:11:20 -08:00
831 changed files with 3438 additions and 24619 deletions

View file

@ -60,10 +60,9 @@ MAILER_SENDER_EMAIL=Chatwoot <accounts@chatwoot.com>
#SMTP domain key is set up for HELO checking #SMTP domain key is set up for HELO checking
SMTP_DOMAIN=chatwoot.com SMTP_DOMAIN=chatwoot.com
# Set the value to "mailhog" if using docker-compose for development environments, # the default value is set "mailhog" and is used by docker-compose for development environments,
# Set the value as "localhost" or your SMTP address in other environments # Set the value as "localhost" or your SMTP address in other environments
# If SMTP_ADDRESS is empty, Chatwoot would try to use sendmail(postfix) SMTP_ADDRESS=mailhog
SMTP_ADDRESS=
SMTP_PORT=1025 SMTP_PORT=1025
SMTP_USERNAME= SMTP_USERNAME=
SMTP_PASSWORD= SMTP_PASSWORD=

2
.gitignore vendored
View file

@ -60,5 +60,3 @@ test/cypress/videos/*
/config/master.key /config/master.key
/config/*.enc /config/*.enc
.vscode/settings.json

View file

@ -16,6 +16,7 @@ Metrics/ClassLength:
- 'app/models/message.rb' - 'app/models/message.rb'
- 'app/builders/messages/facebook/message_builder.rb' - 'app/builders/messages/facebook/message_builder.rb'
- 'app/controllers/api/v1/accounts/contacts_controller.rb' - 'app/controllers/api/v1/accounts/contacts_controller.rb'
- 'app/controllers/api/v1/accounts/conversations_controller.rb'
- 'app/listeners/action_cable_listener.rb' - 'app/listeners/action_cable_listener.rb'
- 'app/models/conversation.rb' - 'app/models/conversation.rb'
RSpec/ExampleLength: RSpec/ExampleLength:

View file

@ -1 +0,0 @@
{}

View file

@ -4,7 +4,7 @@ ruby '3.0.4'
##-- base gems for rails --## ##-- base gems for rails --##
gem 'rack-cors', require: 'rack/cors' gem 'rack-cors', require: 'rack/cors'
gem 'rails', '~> 6.1', '>= 6.1.6.1' gem 'rails', '~>6.1'
# Reduces boot times through caching; required in config/boot.rb # Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', require: false gem 'bootsnap', require: false
@ -56,7 +56,7 @@ gem 'activerecord-import'
gem 'dotenv-rails' gem 'dotenv-rails'
gem 'foreman' gem 'foreman'
gem 'puma' gem 'puma'
gem 'webpacker', '~> 5.4', '>= 5.4.3' gem 'webpacker', '~> 5.x'
# metrics on heroku # metrics on heroku
gem 'barnes' gem 'barnes'
@ -94,7 +94,7 @@ gem 'ddtrace'
gem 'elastic-apm' gem 'elastic-apm'
gem 'newrelic_rpm' gem 'newrelic_rpm'
gem 'scout_apm' gem 'scout_apm'
gem 'sentry-rails', '~> 5.3', '>= 5.3.1' gem 'sentry-rails', '~> 5.3'
gem 'sentry-ruby', '~> 5.3' gem 'sentry-ruby', '~> 5.3'
gem 'sentry-sidekiq', '~> 5.3' gem 'sentry-sidekiq', '~> 5.3'
@ -175,7 +175,7 @@ group :development, :test do
gem 'mock_redis' gem 'mock_redis'
gem 'pry-rails' gem 'pry-rails'
gem 'rspec_junit_formatter' gem 'rspec_junit_formatter'
gem 'rspec-rails', '~> 5.0.3' gem 'rspec-rails', '~> 5.0.0'
gem 'rubocop', require: false gem 'rubocop', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
gem 'rubocop-rails', require: false gem 'rubocop-rails', require: false

View file

@ -398,7 +398,7 @@ GEM
llhttp-ffi (0.4.0) llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0) ffi-compiler (~> 1.0)
rake (~> 13.0) rake (~> 13.0)
loofah (2.19.1) loofah (2.18.0)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
mail (2.7.1) mail (2.7.1)
@ -427,14 +427,14 @@ GEM
netrc (0.11.0) netrc (0.11.0)
newrelic_rpm (8.9.0) newrelic_rpm (8.9.0)
nio4r (2.5.8) nio4r (2.5.8)
nokogiri (1.13.10) nokogiri (1.13.9)
mini_portile2 (~> 2.8.0) mini_portile2 (~> 2.8.0)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-arm64-darwin) nokogiri (1.13.9-arm64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-x86_64-darwin) nokogiri (1.13.9-x86_64-darwin)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.13.10-x86_64-linux) nokogiri (1.13.9-x86_64-linux)
racc (~> 1.4) racc (~> 1.4)
oauth (0.5.10) oauth (0.5.10)
orm_adapter (0.5.0) orm_adapter (0.5.0)
@ -459,7 +459,7 @@ GEM
pundit (2.2.0) pundit (2.2.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
raabro (1.4.0) raabro (1.4.0)
racc (1.6.1) racc (1.6.0)
rack (2.2.4) rack (2.2.4)
rack-attack (6.6.1) rack-attack (6.6.1)
rack (>= 1.0, < 3) rack (>= 1.0, < 3)
@ -488,8 +488,8 @@ GEM
rails-dom-testing (2.0.3) rails-dom-testing (2.0.3)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
nokogiri (>= 1.6) nokogiri (>= 1.6)
rails-html-sanitizer (1.4.4) rails-html-sanitizer (1.4.3)
loofah (~> 2.19, >= 2.19.1) loofah (~> 2.3)
railties (6.1.6.1) railties (6.1.6.1)
actionpack (= 6.1.6.1) actionpack (= 6.1.6.1)
activesupport (= 6.1.6.1) activesupport (= 6.1.6.1)
@ -765,12 +765,12 @@ DEPENDENCIES
rack-attack rack-attack
rack-cors rack-cors
rack-timeout rack-timeout
rails (~> 6.1, >= 6.1.6.1) rails (~> 6.1)
redis redis
redis-namespace redis-namespace
responders responders
rest-client rest-client
rspec-rails (~> 5.0.3) rspec-rails (~> 5.0.0)
rspec_junit_formatter rspec_junit_formatter
rubocop rubocop
rubocop-performance rubocop-performance
@ -778,7 +778,7 @@ DEPENDENCIES
rubocop-rspec rubocop-rspec
scout_apm scout_apm
seed_dump seed_dump
sentry-rails (~> 5.3, >= 5.3.1) sentry-rails (~> 5.3)
sentry-ruby (~> 5.3) sentry-ruby (~> 5.3)
sentry-sidekiq (~> 5.3) sentry-sidekiq (~> 5.3)
shoulda-matchers shoulda-matchers
@ -799,7 +799,7 @@ DEPENDENCIES
valid_email2 valid_email2
web-console web-console
webmock webmock
webpacker (~> 5.4, >= 5.4.3) webpacker (~> 5.x)
webpush webpush
wisper (= 2.0.0) wisper (= 2.0.0)
working_hours working_hours

View file

@ -41,24 +41,16 @@
"formation": { "formation": {
"web": { "web": {
"quantity": 1, "quantity": 1,
"size": "basic" "size": "FREE"
}, },
"worker": { "worker": {
"quantity": 1, "quantity": 1,
"size": "basic" "size": "FREE"
} }
}, },
"stack": "heroku-20", "stack": "heroku-20",
"image": "heroku/ruby", "image": "heroku/ruby",
"addons": [ "addons": [ "heroku-redis", "heroku-postgresql"],
{
"plan": "heroku-redis:mini"
},
{
"plan": "heroku-postgresql:mini"
}
],
"stack": "heroku-20",
"buildpacks": [ "buildpacks": [
{ {
"url": "heroku/ruby" "url": "heroku/ruby"

View file

@ -1,40 +0,0 @@
class ConversationBuilder
pattr_initialize [:params!, :contact_inbox!]
def perform
look_up_exising_conversation || create_new_conversation
end
private
def look_up_exising_conversation
return unless @contact_inbox.inbox.lock_to_single_conversation?
@contact_inbox.conversations.last
end
def create_new_conversation
::Conversation.create!(conversation_params)
end
def conversation_params
additional_attributes = params[:additional_attributes]&.permit! || {}
custom_attributes = params[:custom_attributes]&.permit! || {}
status = params[:status].present? ? { status: params[:status] } : {}
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases
# status = { status: 'pending' } if status[:status] == 'bot'
{
account_id: @contact_inbox.inbox.account_id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: additional_attributes,
custom_attributes: custom_attributes,
snoozed_until: params[:snoozed_until],
assignee_id: params[:assignee_id],
team_id: params[:team_id]
}.merge(status)
end
end

View file

@ -46,7 +46,6 @@ class Messages::Messenger::MessageBuilder
end end
def update_attachment_file_type(attachment) def update_attachment_file_type(attachment)
return if @message.reload.attachments.blank?
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention' return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
attachment.file_type = file_type(attachment.file&.content_type) attachment.file_type = file_type(attachment.file&.content_type)
@ -63,7 +62,6 @@ class Messages::Messenger::MessageBuilder
story_sender = result['from']['username'] story_sender = result['from']['username']
message.content_attributes[:story_sender] = story_sender message.content_attributes[:story_sender] = story_sender
message.content_attributes[:story_id] = story_id message.content_attributes[:story_id] = story_id
message.content_attributes[:image_type] = 'story_mention'
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender) message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
message.save! message.save!
end end
@ -76,7 +74,6 @@ class Messages::Messenger::MessageBuilder
raise raise
rescue Koala::Facebook::ClientError => e rescue Koala::Facebook::ClientError => e
# The exception occurs when we are trying fetch the deleted story or blocked story. # The exception occurs when we are trying fetch the deleted story or blocked story.
@message.attachments.destroy_all
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content')) @message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
Rails.logger.error e Rails.logger.error e
{} {}

View file

@ -2,7 +2,7 @@ class Api::V1::Accounts::Contacts::ConversationsController < Api::V1::Accounts::
def index def index
@conversations = Current.account.conversations.includes( @conversations = Current.account.conversations.includes(
:assignee, :contact, :inbox, :taggings :assignee, :contact, :inbox, :taggings
).where(inbox_id: inbox_ids, contact_id: @contact.id).order(id: :desc).limit(20) ).where(inbox_id: inbox_ids, contact_id: @contact.id)
end end
private private

View file

@ -24,7 +24,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
def create def create
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@conversation = ConversationBuilder.new(params: params, contact_inbox: @contact_inbox).perform @conversation = ::Conversation.create!(conversation_params)
Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present? Messages::MessageBuilder.new(Current.user, @conversation, params[:message]).perform if params[:message].present?
end end
end end
@ -75,13 +75,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
end end
def update_last_seen def update_last_seen
update_last_seen_on_conversation(DateTime.now.utc, assignee?) # rubocop:disable Rails/SkipsModelValidations
end @conversation.update_column(:agent_last_seen_at, DateTime.now.utc)
@conversation.update_column(:assignee_last_seen_at, DateTime.now.utc) if assignee?
def unread # rubocop:enable Rails/SkipsModelValidations
last_incoming_message = @conversation.messages.incoming.last
last_seen_at = last_incoming_message.created_at - 1.second if last_incoming_message.present?
update_last_seen_on_conversation(last_seen_at, true)
end end
def custom_attributes def custom_attributes
@ -91,18 +88,9 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
private private
def update_last_seen_on_conversation(last_seen_at, update_assignee)
# rubocop:disable Rails/SkipsModelValidations
@conversation.update_column(:agent_last_seen_at, last_seen_at)
@conversation.update_column(:assignee_last_seen_at, last_seen_at) if update_assignee.present?
# rubocop:enable Rails/SkipsModelValidations
end
def set_conversation_status def set_conversation_status
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases status = params[:status] == 'bot' ? 'pending' : params[:status]
# commenting this out to see if there are any errors, if not we can remove this in subsequent releases @conversation.status = status
# status = params[:status] == 'bot' ? 'pending' : params[:status]
@conversation.status = params[:status]
@conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until] @conversation.snoozed_until = parse_date_time(params[:snoozed_until].to_s) if params[:snoozed_until]
end end
@ -154,11 +142,31 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
).perform ).perform
end end
def conversation_params
additional_attributes = params[:additional_attributes]&.permit! || {}
custom_attributes = params[:custom_attributes]&.permit! || {}
status = params[:status].present? ? { status: params[:status] } : {}
# TODO: temporary fallback for the old bot status in conversation, we will remove after couple of releases
status = { status: 'pending' } if status[:status] == 'bot'
{
account_id: Current.account.id,
inbox_id: @contact_inbox.inbox_id,
contact_id: @contact_inbox.contact_id,
contact_inbox_id: @contact_inbox.id,
additional_attributes: additional_attributes,
custom_attributes: custom_attributes,
snoozed_until: params[:snoozed_until],
assignee_id: params[:assignee_id],
team_id: params[:team_id]
}.merge(status)
end
def conversation_finder def conversation_finder
@conversation_finder ||= ConversationFinder.new(Current.user, params) @conversation_finder ||= ConversationFinder.new(current_user, params)
end end
def assignee? def assignee?
@conversation.assignee_id? && Current.user == @conversation.assignee @conversation.assignee_id? && current_user == @conversation.assignee
end end
end end

View file

@ -113,8 +113,7 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController
def inbox_attributes def inbox_attributes
[:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled, [:name, :avatar, :greeting_enabled, :greeting_message, :enable_email_collect, :csat_survey_enabled,
:enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved, :enable_auto_assignment, :working_hours_enabled, :out_of_office_message, :timezone, :allow_messages_after_resolved]
:lock_to_single_conversation]
end end
def permitted_params(channel_attributes = []) def permitted_params(channel_attributes = [])

View file

@ -21,7 +21,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def create def create
@portal = Current.account.portals.build(portal_params) @portal = Current.account.portals.build(portal_params)
@portal.custom_domain = parsed_custom_domain
@portal.save! @portal.save!
process_attached_logo process_attached_logo
end end
@ -29,7 +28,6 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def update def update
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@portal.update!(portal_params) if params[:portal].present? @portal.update!(portal_params) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain
process_attached_logo process_attached_logo
rescue StandardError => e rescue StandardError => e
Rails.logger.error e Rails.logger.error e
@ -75,9 +73,4 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
def set_current_page def set_current_page
@current_page = params[:page] || 1 @current_page = params[:page] || 1
end end
def parsed_custom_domain
domain = URI.parse(@portal.custom_domain)
domain.is_a?(URI::HTTP) ? domain.host : @portal.custom_domain
end
end end

View file

@ -18,10 +18,6 @@ class Api::V1::ProfilesController < Api::BaseController
head :ok head :ok
end end
def auto_offline
@user.account_users.find_by!(account_id: auto_offline_params[:account_id]).update!(auto_offline: auto_offline_params[:auto_offline] || false)
end
def availability def availability
@user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability]) @user.account_users.find_by!(account_id: availability_params[:account_id]).update!(availability: availability_params[:availability])
end end
@ -41,10 +37,6 @@ class Api::V1::ProfilesController < Api::BaseController
params.require(:profile).permit(:account_id, :availability) params.require(:profile).permit(:account_id, :availability)
end end
def auto_offline_params
params.require(:profile).permit(:account_id, :auto_offline)
end
def profile_params def profile_params
params.require(:profile).permit( params.require(:profile).permit(
:email, :email,

View file

@ -50,9 +50,7 @@ class Api::V1::Widget::BaseController < ApplicationController
end end
def contact_name def contact_name
return if @contact.email.present? || @contact.phone_number.present? || @contact.identifier.present? params[:contact][:name] || contact_email.split('@')[0] if contact_email.present?
permitted_params.dig(:contact, :name) || (contact_email.split('@')[0] if contact_email.present?)
end end
def contact_phone_number def contact_phone_number

View file

@ -16,7 +16,8 @@ class DashboardController < ActionController::Base
@global_config = GlobalConfig.get( @global_config = GlobalConfig.get(
'LOGO', 'LOGO_THUMBNAIL', 'LOGO', 'LOGO_THUMBNAIL',
'INSTALLATION_NAME', 'INSTALLATION_NAME',
'WIDGET_BRAND_URL', 'TERMS_URL', 'WIDGET_BRAND_URL',
'TERMS_URL',
'PRIVACY_URL', 'PRIVACY_URL',
'DISPLAY_MANIFEST', 'DISPLAY_MANIFEST',
'CREATE_NEW_ACCOUNT_FROM_DASHBOARD', 'CREATE_NEW_ACCOUNT_FROM_DASHBOARD',
@ -24,12 +25,12 @@ class DashboardController < ActionController::Base
'API_CHANNEL_NAME', 'API_CHANNEL_NAME',
'API_CHANNEL_THUMBNAIL', 'API_CHANNEL_THUMBNAIL',
'ANALYTICS_TOKEN', 'ANALYTICS_TOKEN',
'ANALYTICS_HOST',
'DIRECT_UPLOADS_ENABLED', 'DIRECT_UPLOADS_ENABLED',
'HCAPTCHA_SITE_KEY', 'HCAPTCHA_SITE_KEY',
'LOGOUT_REDIRECT_LINK', 'LOGOUT_REDIRECT_LINK',
'DISABLE_USER_PROFILE_UPDATE', 'DISABLE_USER_PROFILE_UPDATE',
'DEPLOYMENT_ENV', 'DEPLOYMENT_ENV'
'CSML_EDITOR_HOST'
).merge(app_config) ).merge(app_config)
end end

View file

@ -1,7 +1,8 @@
class Platform::Api::V1::AccountsController < PlatformController class Platform::Api::V1::AccountsController < PlatformController
def create def create
@resource = Account.create!(account_params) @resource = Account.new(account_params)
update_resource_features update_resource_features
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource) @platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
end end

View file

@ -56,6 +56,7 @@ class ConversationFinder
filter_by_team if @team filter_by_team if @team
filter_by_labels if params[:labels] filter_by_labels if params[:labels]
filter_by_query if params[:q] filter_by_query if params[:q]
filter_by_reply_status
end end
def set_inboxes def set_inboxes
@ -75,9 +76,12 @@ class ConversationFinder
end end
def find_all_conversations def find_all_conversations
@conversations = current_account.conversations.where(inbox_id: @inbox_ids) if params[:conversation_type] == 'mention'
filter_by_conversation_type if params[:conversation_type] conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations @conversations = current_account.conversations.where(id: conversation_ids)
else
@conversations = current_account.conversations.where(inbox_id: @inbox_ids)
end
end end
def filter_by_assignee_type def filter_by_assignee_type
@ -92,15 +96,8 @@ class ConversationFinder
@conversations @conversations
end end
def filter_by_conversation_type def filter_by_reply_status
case @params[:conversation_type] @conversations = @conversations.where(first_reply_created_at: nil) if params[:reply_status] == 'unattended'
when 'mention'
conversation_ids = current_account.mentions.where(user: current_user).pluck(:conversation_id)
@conversations = @conversations.where(id: conversation_ids)
when 'unattended'
@conversations = @conversations.where(first_reply_created_at: nil)
end
@conversations
end end
def filter_by_query def filter_by_query

View file

@ -20,13 +20,22 @@ class MessageFinder
conversation_messages.where.not('private = ? OR message_type = ?', true, 2) conversation_messages.where.not('private = ? OR message_type = ?', true, 2)
end end
def messages_in_desc_order
messages.reorder('created_at desc')
end
def current_messages def current_messages
if @params[:after].present? messages_to_display = messages_in_desc_order
messages.reorder('created_at asc').where('id >= ?', @params[:before].to_i).limit(20)
elsif @params[:before].present? if @params[:before].present?
messages.reorder('created_at desc').where('id < ?', @params[:before].to_i).limit(20).reverse messages_to_display = messages_to_display.where('id < ?', @params[:before].to_i)
else
messages.reorder('created_at desc').limit(20).reverse if @params[:after].present? && @params[:after].to_i < @params[:before].to_i - 25
messages_to_display = messages_to_display.where('id >= ?', @params[:after].to_i)
return messages_to_display.reverse
end
end end
messages_to_display.limit(20).reverse
end end
end end

View file

@ -144,12 +144,6 @@ export default {
}); });
}, },
updateAutoOffline(accountId, autoOffline = false) {
return axios.post(endPoints('autoOffline').url, {
profile: { account_id: accountId, auto_offline: autoOffline },
});
},
deleteAvatar() { deleteAvatar() {
return axios.delete(endPoints('deleteAvatar').url); return axios.delete(endPoints('deleteAvatar').url);
}, },

View file

@ -16,9 +16,6 @@ const endPoints = {
availabilityUpdate: { availabilityUpdate: {
url: '/api/v1/profile/availability', url: '/api/v1/profile/availability',
}, },
autoOffline: {
url: '/api/v1/profile/auto_offline',
},
logout: { logout: {
url: 'auth/sign_out', url: 'auth/sign_out',
}, },

View file

@ -68,10 +68,6 @@ class ConversationApi extends ApiClient {
return axios.post(`${this.url}/${id}/update_last_seen`); return axios.post(`${this.url}/${id}/update_last_seen`);
} }
markMessagesUnread({ id }) {
return axios.post(`${this.url}/${id}/unread`);
}
toggleTyping({ conversationId, status, isPrivate }) { toggleTyping({ conversationId, status, isPrivate }) {
return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, { return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
typing_status: status, typing_status: status,

View file

@ -75,9 +75,9 @@ class MessageApi extends ApiClient {
return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`); return axios.delete(`${this.url}/${conversationID}/messages/${messageId}`);
} }
getPreviousMessages({ conversationId, before }) { getPreviousMessages({ conversationId, after, before }) {
return axios.get(`${this.url}/${conversationId}/messages`, { return axios.get(`${this.url}/${conversationId}/messages`, {
params: { before }, params: { after, before },
}); });
} }
} }

View file

@ -13,16 +13,6 @@ class Inboxes extends ApiClient {
deleteInboxAvatar(inboxId) { deleteInboxAvatar(inboxId) {
return axios.delete(`${this.url}/${inboxId}/avatar`); return axios.delete(`${this.url}/${inboxId}/avatar`);
} }
getAgentBot(inboxId) {
return axios.get(`${this.url}/${inboxId}/agent_bot`);
}
setAgentBot(inboxId, botId) {
return axios.post(`${this.url}/${inboxId}/set_agent_bot`, {
agent_bot: botId,
});
}
} }
export default new Inboxes(); export default new Inboxes();

View file

@ -11,8 +11,6 @@ describe('#InboxesAPI', () => {
expect(inboxesAPI).toHaveProperty('update'); expect(inboxesAPI).toHaveProperty('update');
expect(inboxesAPI).toHaveProperty('delete'); expect(inboxesAPI).toHaveProperty('delete');
expect(inboxesAPI).toHaveProperty('getCampaigns'); expect(inboxesAPI).toHaveProperty('getCampaigns');
expect(inboxesAPI).toHaveProperty('getAgentBot');
expect(inboxesAPI).toHaveProperty('setAgentBot');
}); });
describeWithAPIMock('API calls', context => { describeWithAPIMock('API calls', context => {
it('#getCampaigns', () => { it('#getCampaigns', () => {

View file

@ -1,6 +0,0 @@
/* global axios */
import wootConstants from 'dashboard/constants';
export const getTestimonialContent = () => {
return axios.get(wootConstants.TESTIMONIAL_URL);
};

View file

@ -74,8 +74,8 @@ Tahoma,
Arial, Arial,
sans-serif; sans-serif;
$body-antialiased: true; $body-antialiased: true;
$global-margin: $space-small; $global-margin: $space-one;
$global-padding: $space-micro; $global-padding: $space-one;
$global-weight-normal: normal; $global-weight-normal: normal;
$global-weight-bold: bold; $global-weight-bold: bold;
$global-radius: 0; $global-radius: 0;

View file

@ -20,24 +20,6 @@
@include foundation-everything($flex: true); @include foundation-everything($flex: true);
@include foundation-prototype-text-utilities;
@include foundation-prototype-text-transformation;
@include foundation-prototype-text-decoration;
@include foundation-prototype-font-styling;
@include foundation-prototype-list-style-type;
@include foundation-prototype-rounded;
@include foundation-prototype-bordered;
@include foundation-prototype-shadow;
@include foundation-prototype-separator;
@include foundation-prototype-overflow;
@include foundation-prototype-display;
@include foundation-prototype-position;
@include foundation-prototype-border-box;
@include foundation-prototype-border-none;
@include foundation-prototype-sizing;
@include foundation-prototype-spacing;
@import 'typography'; @import 'typography';
@import 'layout'; @import 'layout';
@import 'animations'; @import 'animations';

View file

@ -155,20 +155,12 @@ $default-button-height: 4.0rem;
// Sizes // Sizes
&.tiny { &.tiny {
height: var(--space-medium); height: var(--space-medium);
.icon+.button__content {
padding-left: var(--space-micro);
}
} }
&.small { &.small {
height: var(--space-large); height: var(--space-large);
padding-bottom: var(--space-smaller); padding-bottom: var(--space-smaller);
padding-top: var(--space-smaller); padding-top: var(--space-smaller);
.icon+.button__content {
padding-left: var(--space-smaller);
}
} }
&.large { &.large {
@ -198,10 +190,6 @@ $default-button-height: 4.0rem;
height: auto; height: auto;
margin: 0; margin: 0;
padding: 0; padding: 0;
&:hover {
text-decoration: underline;
}
} }
} }

View file

@ -59,8 +59,12 @@
.hamburger--menu { .hamburger--menu {
cursor: pointer; cursor: pointer;
display: block; display: none;
margin-right: $space-normal; margin-right: $space-normal;
@media screen and (max-width: 1200px) {
display: block;
}
} }
.header--icon { .header--icon {

View file

@ -102,7 +102,6 @@
@assign-agent="onAssignAgent" @assign-agent="onAssignAgent"
@update-conversations="onUpdateConversations" @update-conversations="onUpdateConversations"
@assign-labels="onAssignLabels" @assign-labels="onAssignLabels"
@assign-team="onAssignTeamsForBulk"
/> />
<div <div
ref="activeConversation" ref="activeConversation"
@ -126,7 +125,6 @@
@assign-label="onAssignLabels" @assign-label="onAssignLabels"
@update-conversation-status="toggleConversationStatus" @update-conversation-status="toggleConversationStatus"
@context-menu-toggle="onContextMenuToggle" @context-menu-toggle="onContextMenuToggle"
@mark-as-unread="markAsUnread"
/> />
<div v-if="chatListLoading" class="text-center"> <div v-if="chatListLoading" class="text-center">
@ -186,11 +184,6 @@ import {
hasPressedAltAndJKey, hasPressedAltAndJKey,
hasPressedAltAndKKey, hasPressedAltAndKKey,
} from 'shared/helpers/KeyboardHelpers'; } from 'shared/helpers/KeyboardHelpers';
import { conversationListPageURL } from '../helper/URLHelper';
import {
isOnMentionsView,
isOnUnattendedView,
} from '../store/modules/conversations/helpers/actionHelpers';
export default { export default {
components: { components: {
@ -339,15 +332,14 @@ export default {
status: this.activeStatus, status: this.activeStatus,
page: this.currentPage + 1, page: this.currentPage + 1,
labels: this.label ? [this.label] : undefined, labels: this.label ? [this.label] : undefined,
teamId: this.teamId || undefined, teamId: this.teamId ? this.teamId : undefined,
conversationType: this.conversationType || undefined, conversationType: this.conversationType
? this.conversationType
: undefined,
folders: this.hasActiveFolders ? this.savedFoldersValue : undefined, folders: this.hasActiveFolders ? this.savedFoldersValue : undefined,
}; };
}, },
pageTitle() { pageTitle() {
if (this.hasAppliedFilters) {
return this.$t('CHAT_LIST.TAB_HEADING');
}
if (this.inbox.name) { if (this.inbox.name) {
return this.inbox.name; return this.inbox.name;
} }
@ -360,9 +352,6 @@ export default {
if (this.conversationType === 'mention') { if (this.conversationType === 'mention') {
return this.$t('CHAT_LIST.MENTION_HEADING'); return this.$t('CHAT_LIST.MENTION_HEADING');
} }
if (this.conversationType === 'unattended') {
return this.$t('CHAT_LIST.UNATTENDED_HEADING');
}
if (this.hasActiveFolders) { if (this.hasActiveFolders) {
return this.activeFolder.name; return this.activeFolder.name;
} }
@ -442,6 +431,9 @@ export default {
}, },
methods: { methods: {
onApplyFilter(payload) { onApplyFilter(payload) {
if (this.$route.name !== 'home') {
this.$router.push({ name: 'home' });
}
this.resetBulkActions(); this.resetBulkActions();
this.foldersQuery = filterQueryGenerator(payload); this.foldersQuery = filterQueryGenerator(payload);
this.$store.dispatch('conversationPage/reset'); this.$store.dispatch('conversationPage/reset');
@ -644,35 +636,6 @@ export default {
this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED')); this.showAlert(this.$t('BULK_ACTION.ASSIGN_FAILED'));
} }
}, },
async markAsUnread(conversationId) {
try {
await this.$store.dispatch('markMessagesUnread', {
id: conversationId,
});
const {
params: { accountId, inbox_id: inboxId, label, teamId },
name,
} = this.$route;
let conversationType = '';
if (isOnMentionsView({ route: { name } })) {
conversationType = 'mention';
} else if (isOnUnattendedView({ route: { name } })) {
conversationType = 'unattended';
}
this.$router.push(
conversationListPageURL({
accountId,
conversationType: conversationType,
customViewId: this.foldersId,
inboxId,
label,
teamId,
})
);
} catch (error) {
// Ignore error
}
},
async onAssignTeam(team, conversationId = null) { async onAssignTeam(team, conversationId = null) {
try { try {
await this.$store.dispatch('assignTeam', { await this.$store.dispatch('assignTeam', {
@ -722,21 +685,6 @@ export default {
this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED')); this.showAlert(this.$t('BULK_ACTION.LABELS.ASSIGN_FAILED'));
} }
}, },
async onAssignTeamsForBulk(team) {
try {
await this.$store.dispatch('bulkActions/process', {
type: 'Conversation',
ids: this.selectedConversations,
fields: {
team_id: team.id,
},
});
this.selectedConversations = [];
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_SUCCESFUL'));
} catch (err) {
this.showAlert(this.$t('BULK_ACTION.TEAMS.ASSIGN_FAILED'));
}
},
async onUpdateConversations(status) { async onUpdateConversations(status) {
try { try {
await this.$store.dispatch('bulkActions/process', { await this.$store.dispatch('bulkActions/process', {

View file

@ -1,12 +1,7 @@
<template> <template>
<woot-button <button @click="onMenuItemClick">
size="small" <fluent-icon class="hamburger--menu" icon="list" />
variant="clear" </button>
color-scheme="secondary"
icon="list"
class="toggle-sidebar"
@click="onMenuItemClick"
/>
</template> </template>
<script> <script>
@ -21,8 +16,13 @@ export default {
}; };
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.toggle-sidebar { .hamburger--menu {
margin-right: var(--space-small); cursor: pointer;
margin-left: var(--space-minus-small); display: none;
margin-right: var(--space-normal);
@media screen and (max-width: 1200px) {
display: block;
}
} }
</style> </style>

View file

@ -18,35 +18,12 @@
</woot-button> </woot-button>
</woot-dropdown-item> </woot-dropdown-item>
<woot-dropdown-divider /> <woot-dropdown-divider />
<woot-dropdown-item class="auto-offline--toggle">
<div class="info-wrap">
<fluent-icon
v-tooltip.right-start="$t('SIDEBAR.SET_AUTO_OFFLINE.INFO_TEXT')"
icon="info"
size="14"
class="info-icon"
/>
<span class="auto-offline--text">
{{ $t('SIDEBAR.SET_AUTO_OFFLINE.TEXT') }}
</span>
</div>
<woot-switch
size="small"
class="auto-offline--switch"
:value="currentUserAutoOffline"
@input="updateAutoOffline"
/>
</woot-dropdown-item>
<woot-dropdown-divider />
</woot-dropdown-menu> </woot-dropdown-menu>
</template> </template>
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu';
import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader'; import WootDropdownHeader from 'shared/components/ui/dropdown/DropdownHeader';
@ -64,7 +41,7 @@ export default {
AvailabilityStatusBadge, AvailabilityStatusBadge,
}, },
mixins: [clickaway, alertMixin], mixins: [clickaway],
data() { data() {
return { return {
@ -77,7 +54,6 @@ export default {
...mapGetters({ ...mapGetters({
getCurrentUserAvailability: 'getCurrentUserAvailability', getCurrentUserAvailability: 'getCurrentUserAvailability',
currentAccountId: 'getCurrentAccountId', currentAccountId: 'getCurrentAccountId',
currentUserAutoOffline: 'getCurrentUserAutoOffline',
}), }),
availabilityDisplayLabel() { availabilityDisplayLabel() {
const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex( const availabilityIndex = AVAILABILITY_STATUS_KEYS.findIndex(
@ -109,30 +85,21 @@ export default {
closeStatusMenu() { closeStatusMenu() {
this.isStatusMenuOpened = false; this.isStatusMenuOpened = false;
}, },
updateAutoOffline(autoOffline) {
this.$store.dispatch('updateAutoOffline', {
accountId: this.currentAccountId,
autoOffline,
});
},
changeAvailabilityStatus(availability) { changeAvailabilityStatus(availability) {
const accountId = this.currentAccountId;
if (this.isUpdating) { if (this.isUpdating) {
return; return;
} }
this.isUpdating = true; this.isUpdating = true;
try { this.$store
this.$store.dispatch('updateAvailability', { .dispatch('updateAvailability', {
availability, availability: availability,
account_id: this.currentAccountId, account_id: accountId,
})
.finally(() => {
this.isUpdating = false;
}); });
} catch (error) {
this.showAlert(
this.$t('PROFILE_SETTINGS.FORM.AVAILABILITY.SET_AVAILABILITY_ERROR')
);
} finally {
this.isUpdating = false;
}
}, },
}, },
}; };
@ -176,32 +143,4 @@ export default {
align-items: baseline; align-items: baseline;
} }
} }
.auto-offline--toggle {
align-items: center;
display: flex;
justify-content: space-between;
padding: var(--space-smaller) 0 var(--space-smaller) var(--space-small);
margin: 0;
.info-wrap {
display: flex;
align-items: center;
}
.info-icon {
margin-top: -1px;
}
.auto-offline--switch {
margin: -1px var(--space-micro) 0;
}
.auto-offline--text {
margin: 0 var(--space-smaller);
font-size: var(--font-size-mini);
font-weight: var(--font-weight-medium);
color: var(--s-700);
}
}
</style> </style>

View file

@ -16,8 +16,6 @@ const conversations = accountId => ({
'conversation_through_mentions', 'conversation_through_mentions',
'folder_conversations', 'folder_conversations',
'conversations_through_folders', 'conversations_through_folders',
'conversation_unattended',
'conversation_through_unattended',
], ],
menuItems: [ menuItems: [
{ {
@ -35,13 +33,6 @@ const conversations = accountId => ({
toState: frontendURL(`accounts/${accountId}/mentions/conversations`), toState: frontendURL(`accounts/${accountId}/mentions/conversations`),
toStateName: 'conversation_mentions', toStateName: 'conversation_mentions',
}, },
{
icon: 'mail-unread',
label: 'UNATTENDED_CONVERSATIONS',
key: 'conversation_unattended',
toState: frontendURL(`accounts/${accountId}/unattended/conversations`),
toStateName: 'conversation_unattended',
},
], ],
}); });

View file

@ -1,4 +1,3 @@
import { FEATURE_FLAGS } from '../../../../featureFlags';
import { frontendURL } from '../../../../helper/URLHelper'; import { frontendURL } from '../../../../helper/URLHelper';
const primaryMenuItems = accountId => [ const primaryMenuItems = accountId => [
@ -14,7 +13,6 @@ const primaryMenuItems = accountId => [
icon: 'book-contacts', icon: 'book-contacts',
key: 'contacts', key: 'contacts',
label: 'CONTACTS', label: 'CONTACTS',
featureFlag: FEATURE_FLAGS.CRM,
toState: frontendURL(`accounts/${accountId}/contacts`), toState: frontendURL(`accounts/${accountId}/contacts`),
toStateName: 'contacts_dashboard', toStateName: 'contacts_dashboard',
roles: ['administrator', 'agent'], roles: ['administrator', 'agent'],
@ -23,7 +21,6 @@ const primaryMenuItems = accountId => [
icon: 'arrow-trending-lines', icon: 'arrow-trending-lines',
key: 'reports', key: 'reports',
label: 'REPORTS', label: 'REPORTS',
featureFlag: FEATURE_FLAGS.REPORTS,
toState: frontendURL(`accounts/${accountId}/reports`), toState: frontendURL(`accounts/${accountId}/reports`),
toStateName: 'settings_account_reports', toStateName: 'settings_account_reports',
roles: ['administrator'], roles: ['administrator'],
@ -32,7 +29,6 @@ const primaryMenuItems = accountId => [
icon: 'megaphone', icon: 'megaphone',
key: 'campaigns', key: 'campaigns',
label: 'CAMPAIGNS', label: 'CAMPAIGNS',
featureFlag: FEATURE_FLAGS.CAMPAIGNS,
toState: frontendURL(`accounts/${accountId}/campaigns`), toState: frontendURL(`accounts/${accountId}/campaigns`),
toStateName: 'settings_account_campaigns', toStateName: 'settings_account_campaigns',
roles: ['administrator'], roles: ['administrator'],
@ -41,7 +37,7 @@ const primaryMenuItems = accountId => [
icon: 'library', icon: 'library',
key: 'helpcenter', key: 'helpcenter',
label: 'HELP_CENTER.TITLE', label: 'HELP_CENTER.TITLE',
featureFlag: FEATURE_FLAGS.HELP_CENTER, featureFlag: 'help_center',
toState: frontendURL(`accounts/${accountId}/portals`), toState: frontendURL(`accounts/${accountId}/portals`),
toStateName: 'default_portal_articles', toStateName: 'default_portal_articles',
roles: ['administrator'], roles: ['administrator'],

View file

@ -102,7 +102,6 @@ const settings = accountId => ({
label: 'AGENT_BOTS', label: 'AGENT_BOTS',
beta: true, beta: true,
hasSubMenu: false, hasSubMenu: false,
globalConfigFlag: 'csmlEditorHost',
toState: frontendURL(`accounts/${accountId}/settings/agent-bots`), toState: frontendURL(`accounts/${accountId}/settings/agent-bots`),
toStateName: 'agent_bots', toStateName: 'agent_bots',
featureFlag: FEATURE_FLAGS.AGENT_BOTS, featureFlag: FEATURE_FLAGS.AGENT_BOTS,

View file

@ -61,24 +61,6 @@
</a> </a>
</router-link> </router-link>
</woot-dropdown-item> </woot-dropdown-item>
<woot-dropdown-item v-if="currentUser.type === 'SuperAdmin'">
<a
href="/super_admin"
class="button small clear secondary"
target="_blank"
rel="noopener nofollow noreferrer"
@click="$emit('close')"
>
<fluent-icon
icon="content-settings"
size="14"
class="icon icon--font"
/>
<span class="button__content">
{{ $t('SIDEBAR_ITEMS.SUPER_ADMIN_CONSOLE') }}
</span>
</a>
</woot-dropdown-item>
<woot-dropdown-item> <woot-dropdown-item>
<woot-button <woot-button
variant="clear" variant="clear"
@ -153,7 +135,7 @@ export default {
.dropdown-pane { .dropdown-pane {
left: var(--space-slab); left: var(--space-slab);
bottom: var(--space-larger); bottom: var(--space-larger);
min-width: 22rem; min-width: 16.8rem;
z-index: var(--z-index-low); z-index: var(--z-index-much-higher);
} }
</style> </style>

View file

@ -261,7 +261,14 @@ export default {
width: 20rem; width: 20rem;
flex-shrink: 0; flex-shrink: 0;
overflow-y: hidden; overflow-y: hidden;
position: unset;
@include breakpoint(xlarge down) {
position: absolute;
}
@include breakpoint(xlarge up) {
position: unset;
}
&:hover { &:hover {
overflow-y: hidden; overflow-y: hidden;

View file

@ -112,7 +112,6 @@ $label-badge-size: var(--space-slab);
padding: var(--space-smaller) var(--space-smaller); padding: var(--space-smaller) var(--space-smaller);
margin: var(--space-smaller) 0; margin: var(--space-smaller) 0;
text-align: left; text-align: left;
line-height: 1.2;
&:hover { &:hover {
background: var(--s-25); background: var(--s-25);
@ -136,6 +135,8 @@ $label-badge-size: var(--space-slab);
.menu-label { .menu-label {
flex-grow: 1; flex-grow: 1;
display: inline-flex;
align-items: center;
} }
.inbox-icon { .inbox-icon {

View file

@ -87,10 +87,6 @@ import {
} from 'dashboard/helper/inbox'; } from 'dashboard/helper/inbox';
import SecondaryChildNavItem from './SecondaryChildNavItem'; import SecondaryChildNavItem from './SecondaryChildNavItem';
import {
isOnMentionsView,
isOnUnattendedView,
} from '../../../store/modules/conversations/helpers/actionHelpers';
export default { export default {
components: { SecondaryChildNavItem }, components: { SecondaryChildNavItem },
@ -106,48 +102,32 @@ export default {
activeInbox: 'getSelectedInbox', activeInbox: 'getSelectedInbox',
accountId: 'getCurrentAccountId', accountId: 'getCurrentAccountId',
isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount', isFeatureEnabledonAccount: 'accounts/isFeatureEnabledonAccount',
globalConfig: 'globalConfig/get',
}), }),
hasSubMenu() { hasSubMenu() {
return !!this.menuItem.children; return !!this.menuItem.children;
}, },
isMenuItemVisible() { isMenuItemVisible() {
if (this.menuItem.globalConfigFlag) { if (!this.menuItem.featureFlag) {
return !!this.globalConfig[this.menuItem.globalConfigFlag]; return true;
} }
if (this.menuItem.featureFlag) { return this.isFeatureEnabledonAccount(
return this.isFeatureEnabledonAccount( this.accountId,
this.accountId, this.menuItem.featureFlag
this.menuItem.featureFlag );
);
}
return true;
}, },
isAllConversations() { isInboxConversation() {
return ( return (
this.$store.state.route.name === 'inbox_conversation' && this.$store.state.route.name === 'inbox_conversation' &&
this.menuItem.toStateName === 'home' this.menuItem.toStateName === 'home'
); );
}, },
isMentions() {
return (
isOnMentionsView({ route: this.$route }) &&
this.menuItem.toStateName === 'conversation_mentions'
);
},
isUnattended() {
return (
isOnUnattendedView({ route: this.$route }) &&
this.menuItem.toStateName === 'conversation_unattended'
);
},
isTeamsSettings() { isTeamsSettings() {
return ( return (
this.$store.state.route.name === 'settings_teams_edit' && this.$store.state.route.name === 'settings_teams_edit' &&
this.menuItem.toStateName === 'settings_teams_list' this.menuItem.toStateName === 'settings_teams_list'
); );
}, },
isInboxSettings() { isInboxsSettings() {
return ( return (
this.$store.state.route.name === 'settings_inbox_show' && this.$store.state.route.name === 'settings_inbox_show' &&
this.menuItem.toStateName === 'settings_inbox_list' this.menuItem.toStateName === 'settings_inbox_list'
@ -170,20 +150,14 @@ export default {
}, },
computedClass() { computedClass() {
// If active inbox is present, do not highlight conversations // If active Inbox is present
// donot highlight conversations
if (this.activeInbox) return ' '; if (this.activeInbox) return ' ';
if (
this.isAllConversations ||
this.isMentions ||
this.isUnattended ||
this.isCurrentRoute
) {
return 'is-active';
}
if (this.hasSubMenu) { if (this.hasSubMenu) {
if ( if (
this.isInboxConversation ||
this.isTeamsSettings || this.isTeamsSettings ||
this.isInboxSettings || this.isInboxsSettings ||
this.isIntegrationsSettings || this.isIntegrationsSettings ||
this.isApplicationsSettings this.isApplicationsSettings
) { ) {
@ -192,6 +166,10 @@ export default {
return ' '; return ' ';
} }
if (this.isCurrentRoute) {
return 'is-active';
}
return ''; return '';
}, },
}, },

View file

@ -1,11 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SidemenuIcon matches snapshot 1`] = ` exports[`SidemenuIcon matches snapshot 1`] = `
<woot-button <button>
class="toggle-sidebar" <fluent-icon
color-scheme="secondary" class="hamburger--menu"
icon="list" icon="list"
size="small" />
variant="clear" </button>
/>
`; `;

View file

@ -4,7 +4,7 @@
<fluent-icon :icon="icon" size="12" class="label--icon" /> <fluent-icon :icon="icon" size="12" class="label--icon" />
</span> </span>
<span <span
v-if="variant === 'smooth' && title && !icon" v-if="variant === 'smooth'"
:style="{ background: color }" :style="{ background: color }"
class="label-color-dot" class="label-color-dot"
/> />
@ -117,16 +117,14 @@ export default {
height: var(--space-medium); height: var(--space-medium);
&.small { &.small {
font-size: var(--font-size-mini); font-size: var(--font-size-micro);
padding: var(--space-micro) var(--space-smaller); padding: var(--space-micro) var(--space-smaller);
line-height: 1.2; line-height: 1.2;
height: var(--space-two); letter-spacing: 0.15px;
} }
.label--icon { .label--icon {
cursor: pointer; cursor: pointer;
}
.label-color-dot {
margin-right: var(--space-smaller); margin-right: var(--space-smaller);
} }
@ -201,8 +199,8 @@ export default {
&.smooth { &.smooth {
background: transparent; background: transparent;
border: 1px solid var(--s-100); border: 1px solid var(--s-75);
color: var(--s-700); color: var(--s-800);
} }
} }
@ -223,22 +221,14 @@ export default {
} }
.label-action--button { .label-action--button {
display: flex; margin-bottom: var(--space-minus-micro);
margin-right: var(--space-smaller);
} }
.label-color-dot { .label-color-dot {
display: inline-block; display: inline-block;
width: var(--space-slab); width: var(--space-one);
height: var(--space-slab); height: var(--space-one);
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
margin-right: var(--space-smaller); margin-right: var(--space-smaller);
box-shadow: var(--shadow-small);
}
.label.small .label-color-dot {
width: var(--space-small);
height: var(--space-small);
border-radius: var(--border-radius-small);
box-shadow: var(--shadow-small);
} }
</style> </style>

View file

@ -2,7 +2,7 @@
<button <button
type="button" type="button"
class="toggle-button" class="toggle-button"
:class="{ active: value, small: size === 'small' }" :class="{ active: value }"
role="switch" role="switch"
:aria-checked="value.toString()" :aria-checked="value.toString()"
@click="onClick" @click="onClick"
@ -15,7 +15,6 @@
export default { export default {
props: { props: {
value: { type: Boolean, default: false }, value: { type: Boolean, default: false },
size: { type: String, default: '' },
}, },
methods: { methods: {
onClick() { onClick() {
@ -46,20 +45,6 @@ export default {
background-color: var(--w-500); background-color: var(--w-500);
} }
&.small {
width: 22px;
height: 14px;
span {
height: var(--space-one);
width: var(--space-one);
&.active {
transform: translate(var(--space-small), var(--space-zero));
}
}
}
span { span {
--space-one-point-five: 1.5rem; --space-one-point-five: 1.5rem;
background-color: var(--white); background-color: var(--white);

View file

@ -18,32 +18,14 @@
<div v-if="showActionInput" class="filter__answer--wrap"> <div v-if="showActionInput" class="filter__answer--wrap">
<div v-if="inputType"> <div v-if="inputType">
<div <div
v-if="inputType === 'search_select'" v-if="inputType === 'multi_select'"
class="multiselect-wrap--small" class="multiselect-wrap--small"
> >
<multiselect <multiselect
v-model="action_params" v-model="action_params"
track-by="id" track-by="id"
label="name" label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')" :placeholder="'Select'"
selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
deselect-label=""
:max-height="160"
:options="dropdownValues"
:allow-empty="false"
:option-height="104"
/>
</div>
<div
v-else-if="inputType === 'multi_select'"
class="multiselect-wrap--small"
>
<multiselect
v-model="action_params"
track-by="id"
label="name"
:placeholder="$t('FORMS.MULTISELECT.SELECT')"
:multiple="true" :multiple="true"
selected-label selected-label
:select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')" :select-label="$t('FORMS.MULTISELECT.ENTER_TO_SELECT')"
@ -51,7 +33,6 @@
:max-height="160" :max-height="160"
:options="dropdownValues" :options="dropdownValues"
:allow-empty="false" :allow-empty="false"
:option-height="104"
/> />
</div> </div>
<input <input
@ -279,6 +260,6 @@ export default {
margin-bottom: var(--space-zero); margin-bottom: var(--space-zero);
} }
.action-message { .action-message {
margin: var(--space-small) var(--space-zero) var(--space-zero); margin: var(--space-small) 0 0;
} }
</style> </style>

View file

@ -48,6 +48,5 @@ export default {
text-align: center; text-align: center;
background-image: linear-gradient(to top, var(--w-100) 0%, var(--w-75) 100%); background-image: linear-gradient(to top, var(--w-100) 0%, var(--w-75) 100%);
color: var(--w-600); color: var(--w-600);
cursor: default;
} }
</style> </style>

View file

@ -67,9 +67,6 @@ export default {
if (Object.keys(this.enabledFeatures).length === 0) { if (Object.keys(this.enabledFeatures).length === 0) {
return false; return false;
} }
if (key === 'website') {
return this.enabledFeatures.channel_website;
}
if (key === 'facebook') { if (key === 'facebook') {
return this.enabledFeatures.channel_facebook; return this.enabledFeatures.channel_facebook;
} }

View file

@ -61,7 +61,6 @@ export default {
} }
.colorpicker--selected { .colorpicker--selected {
border: 1px solid var(--color-border-light);
border-radius: $space-smaller; border-radius: $space-smaller;
cursor: pointer; cursor: pointer;
height: $space-large; height: $space-large;

View file

@ -46,16 +46,11 @@ export default {
return { return {
conversation: this.currentChat, conversation: this.currentChat,
contact: this.$store.getters['contacts/getContact'](this.contactId), contact: this.$store.getters['contacts/getContact'](this.contactId),
currentAgent: this.currentAgent,
}; };
}, },
contactId() { contactId() {
return this.currentChat?.meta?.sender?.id; return this.currentChat?.meta?.sender?.id;
}, },
currentAgent() {
const { id, name, email } = this.$store.getters.getCurrentUser;
return { id, name, email };
},
}, },
mounted() { mounted() {

View file

@ -32,7 +32,6 @@
v-for="attribute in filterAttributes" v-for="attribute in filterAttributes"
:key="attribute.key" :key="attribute.key"
:value="attribute.key" :value="attribute.key"
:disabled="attribute.disabled"
> >
{{ attribute.name }} {{ attribute.name }}
</option> </option>
@ -174,10 +173,6 @@ export default {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
customAttributeType: {
type: String,
default: '',
},
}, },
computed: { computed: {
attributeKey: { attributeKey: {

View file

@ -1,11 +1,7 @@
<template> <template>
<span> <span>
{{ textToBeDisplayed }} {{ textToBeDisplayed }}
<button <button class="show-more--button" @click="toggleShowMore">
v-if="text.length > limit"
class="show-more--button"
@click="toggleShowMore"
>
{{ buttonLabel }} {{ buttonLabel }}
</button> </button>
</span> </span>
@ -29,7 +25,7 @@ export default {
}, },
computed: { computed: {
textToBeDisplayed() { textToBeDisplayed() {
if (this.showMore || this.text.length <= this.limit) { if (this.showMore) {
return this.text; return this.text;
} }

View file

@ -1,9 +1,5 @@
<template> <template>
<div <div :class="thumbnailBoxClass" :style="{ height: size, width: size }">
:class="thumbnailBoxClass"
:style="{ height: size, width: size }"
:title="title"
>
<!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. --> <!-- Using v-show instead of v-if to avoid flickering as v-if removes dom elements. -->
<img <img
v-show="shouldShowImage" v-show="shouldShowImage"
@ -76,10 +72,6 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
title: {
type: String,
default: '',
},
variant: { variant: {
type: String, type: String,
default: 'circle', default: 'circle',

View file

@ -3,13 +3,11 @@
<thumbnail <thumbnail
v-for="user in usersList" v-for="user in usersList"
:key="user.id" :key="user.id"
v-tooltip="user.name"
:title="user.name"
:src="user.thumbnail" :src="user.thumbnail"
:username="user.name" :username="user.name"
:has-border="true" :has-border="true"
:size="size" :size="size"
:class="`overlapping-thumbnail gap-${gap}`" class="overlapping-thumbnail"
/> />
<span v-if="showMoreThumbnailsCount" class="thumbnail-more-text"> <span v-if="showMoreThumbnailsCount" class="thumbnail-more-text">
{{ moreThumbnailsText }} {{ moreThumbnailsText }}
@ -40,14 +38,6 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
gap: {
type: String,
default: 'normal',
validator(value) {
// The value must match one of these strings
return ['normal', '', 'tight'].includes(value);
},
},
}, },
}; };
</script> </script>
@ -62,10 +52,6 @@ export default {
box-shadow: var(--shadow-small); box-shadow: var(--shadow-small);
&:not(:first-child) { &:not(:first-child) {
margin-left: var(--space-minus-smaller);
}
.gap-tight {
margin-left: var(--space-minus-small); margin-left: var(--space-minus-small);
} }
} }

View file

@ -10,11 +10,11 @@ import 'videojs-record/dist/css/videojs.record.css';
import videojs from 'video.js'; import videojs from 'video.js';
import inboxMixin from '../../../../shared/mixins/inboxMixin';
import alertMixin from '../../../../shared/mixins/alertMixin'; import alertMixin from '../../../../shared/mixins/alertMixin';
import Recorder from 'opus-recorder'; import Recorder from 'opus-recorder';
import encoderWorker from 'opus-recorder/dist/encoderWorker.min'; import encoderWorker from 'opus-recorder/dist/encoderWorker.min';
import waveWorker from 'opus-recorder/dist/waveWorker.min';
import WaveSurfer from 'wavesurfer.js'; import WaveSurfer from 'wavesurfer.js';
import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js'; import MicrophonePlugin from 'wavesurfer.js/dist/plugin/wavesurfer.microphone.js';
@ -23,25 +23,19 @@ import 'videojs-wavesurfer/dist/videojs.wavesurfer.js';
import 'videojs-record/dist/videojs.record.js'; import 'videojs-record/dist/videojs.record.js';
import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js'; import 'videojs-record/dist/plugins/videojs.record.opus-recorder.js';
import { format, addSeconds } from 'date-fns'; import { format, addSeconds } from 'date-fns';
import { AUDIO_FORMATS } from 'shared/constants/messages';
WaveSurfer.microphone = MicrophonePlugin; WaveSurfer.microphone = MicrophonePlugin;
export default { export default {
name: 'WootAudioRecorder', name: 'WootAudioRecorder',
mixins: [alertMixin], mixins: [inboxMixin, alertMixin],
props: {
audioRecordFormat: {
type: String,
default: AUDIO_FORMATS.WEBM,
},
},
data() { data() {
return { return {
player: false, player: false,
recordingDateStarted: new Date(0), recordingDateStarted: new Date(0),
initialTimeDuration: '00:00', initialTimeDuration: '00:00',
recorderOptions: { recorderOptions: {
debug: true,
controls: true, controls: true,
bigPlayButton: false, bigPlayButton: false,
fluid: false, fluid: false,
@ -76,28 +70,13 @@ export default {
record: { record: {
audio: true, audio: true,
video: false, video: false,
maxLength: 900, displayMilliseconds: false,
timeSlice: 1000, maxLength: 300,
maxFileSize: 15 * 1024 * 1024, audioEngine: 'opus-recorder',
...(this.audioRecordFormat === AUDIO_FORMATS.WEBM && { audioWorkerURL: encoderWorker,
monitorGain: 0, audioChannels: 1,
recordingGain: 1, audioSampleRate: 48000,
numberOfChannels: 1, audioBitRate: 128,
encoderSampleRate: 16000,
originalSampleRateOverride: 16000,
streamPages: true,
maxFramesPerPage: 1,
encoderFrameSize: 1,
encoderPath: waveWorker,
}),
...(this.audioRecordFormat === AUDIO_FORMATS.OGG && {
displayMilliseconds: false,
audioEngine: 'opus-recorder',
audioWorkerURL: encoderWorker,
audioChannels: 1,
audioSampleRate: 48000,
audioBitRate: 128,
}),
}, },
}, },
}, },

View file

@ -39,17 +39,10 @@ const TYPING_INDICATOR_IDLE_TIME = 4000;
import '@chatwoot/prosemirror-schema/src/woot-editor.css'; import '@chatwoot/prosemirror-schema/src/woot-editor.css';
import { import {
hasPressedEnterAndNotCmdOrShift,
hasPressedCommandAndEnter,
hasPressedAltAndPKey, hasPressedAltAndPKey,
hasPressedAltAndLKey, hasPressedAltAndLKey,
} from 'shared/helpers/KeyboardHelpers'; } from 'shared/helpers/KeyboardHelpers';
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import AnalyticsHelper, {
ANALYTICS_EVENTS,
} from '../../../helper/AnalyticsHelper';
const createState = (content, placeholder, plugins = []) => { const createState = (content, placeholder, plugins = []) => {
return EditorState.create({ return EditorState.create({
@ -65,15 +58,13 @@ const createState = (content, placeholder, plugins = []) => {
export default { export default {
name: 'WootMessageEditor', name: 'WootMessageEditor',
components: { TagAgents, CannedResponse }, components: { TagAgents, CannedResponse },
mixins: [eventListenerMixins, uiSettingsMixin], mixins: [eventListenerMixins],
props: { props: {
value: { type: String, default: '' }, value: { type: String, default: '' },
editorId: { type: String, default: '' }, editorId: { type: String, default: '' },
placeholder: { type: String, default: '' }, placeholder: { type: String, default: '' },
isPrivate: { type: Boolean, default: false }, isPrivate: { type: Boolean, default: false },
enableSuggestions: { type: Boolean, default: true }, enableSuggestions: { type: Boolean, default: true },
overrideLineBreaks: { type: Boolean, default: false },
updateSelectionWith: { type: String, default: '' },
}, },
data() { data() {
return { return {
@ -171,25 +162,6 @@ export default {
isPrivate() { isPrivate() {
this.reloadState(); this.reloadState();
}, },
updateSelectionWith(newValue, oldValue) {
if (!this.editorView) {
return null;
}
if (newValue !== oldValue) {
if (this.updateSelectionWith !== '') {
const node = this.editorView.state.schema.text(
this.updateSelectionWith
);
const tr = this.editorView.state.tr.replaceSelectionWith(node);
this.editorView.focus();
this.state = this.editorView.state.apply(tr);
this.emitOnChange();
this.$emit('clear-selection');
}
}
return null;
},
}, },
created() { created() {
this.state = createState(this.value, this.placeholder, this.plugins); this.state = createState(this.value, this.placeholder, this.plugins);
@ -216,9 +188,6 @@ export default {
keyup: () => { keyup: () => {
this.onKeyup(); this.onKeyup();
}, },
keydown: (view, event) => {
this.onKeydown(event);
},
focus: () => { focus: () => {
this.onFocus(); this.onFocus();
}, },
@ -234,12 +203,6 @@ export default {
}, },
}); });
}, },
isEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'enter');
},
isCmdPlusEnterToSendEnabled() {
return isEditorHotKeyEnabled(this.uiSettings, 'cmd_enter');
},
handleKeyEvents(e) { handleKeyEvents(e) {
if (hasPressedAltAndPKey(e)) { if (hasPressedAltAndPKey(e)) {
this.focusEditorInputField(); this.focusEditorInputField();
@ -270,10 +233,7 @@ export default {
node node
); );
this.state = this.editorView.state.apply(tr); this.state = this.editorView.state.apply(tr);
this.emitOnChange(); return this.emitOnChange();
AnalyticsHelper.track(ANALYTICS_EVENTS.USED_MENTIONS);
return false;
}, },
insertCannedResponse(cannedItem) { insertCannedResponse(cannedItem) {
@ -281,27 +241,22 @@ export default {
return null; return null;
} }
let from = this.range.from - 1; const tr = this.editorView.state.tr.insertText(
let node = addMentionsToMarkdownParser(defaultMarkdownParser).parse( cannedItem,
cannedItem this.range.from,
this.range.to
); );
if (node.childCount === 1) {
node = this.editorView.state.schema.text(cannedItem);
from = this.range.from;
}
const tr = this.editorView.state.tr.replaceWith(
from,
this.range.to,
node
);
this.state = this.editorView.state.apply(tr); this.state = this.editorView.state.apply(tr);
this.emitOnChange(); this.emitOnChange();
tr.scrollIntoView(); // Hacky fix for #5501
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE); this.state = createState(
this.contentFromEditor,
this.placeholder,
this.plugins
);
this.editorView.updateState(this.state);
this.focusEditorInputField();
return false; return false;
}, },
@ -323,24 +278,6 @@ export default {
clearTimeout(this.idleTimer); clearTimeout(this.idleTimer);
} }
}, },
handleLineBreakWhenEnterToSendEnabled(event) {
if (
hasPressedEnterAndNotCmdOrShift(event) &&
this.isEnterToSendEnabled() &&
!this.overrideLineBreaks
) {
event.preventDefault();
}
},
handleLineBreakWhenCmdAndEnterToSendEnabled(event) {
if (
hasPressedCommandAndEnter(event) &&
this.isCmdPlusEnterToSendEnabled() &&
!this.overrideLineBreaks
) {
event.preventDefault();
}
},
onKeyup() { onKeyup() {
if (!this.idleTimer) { if (!this.idleTimer) {
this.$emit('typing-on'); this.$emit('typing-on');
@ -351,14 +288,6 @@ export default {
TYPING_INDICATOR_IDLE_TIME TYPING_INDICATOR_IDLE_TIME
); );
}, },
onKeydown(event) {
if (this.isEnterToSendEnabled()) {
this.handleLineBreakWhenEnterToSendEnabled(event);
}
if (this.isCmdPlusEnterToSendEnabled()) {
this.handleLineBreakWhenCmdAndEnterToSendEnabled(event);
}
},
onBlur() { onBlur() {
this.turnOffIdleTimer(); this.turnOffIdleTimer();
this.resetTyping(); this.resetTyping();

View file

@ -232,18 +232,11 @@ export default {
return this.showFileUpload || this.isNote; return this.showFileUpload || this.isNote;
}, },
showAudioRecorderButton() { showAudioRecorderButton() {
// Disable audio recorder for safari browser as recording is not supported
const isSafari = /^((?!chrome|android|crios|fxios).)*safari/i.test(
navigator.userAgent
);
return ( return (
this.isFeatureEnabledonAccount( this.isFeatureEnabledonAccount(
this.accountId, this.accountId,
FEATURE_FLAGS.VOICE_RECORDER FEATURE_FLAGS.VOICE_RECORDER
) && ) && this.showAudioRecorder
this.showAudioRecorder &&
!isSafari
); );
}, },
showAudioPlayStopButton() { showAudioPlayStopButton() {

View file

@ -91,7 +91,6 @@
</span> </span>
<span class="unread">{{ unreadCount > 9 ? '9+' : unreadCount }}</span> <span class="unread">{{ unreadCount > 9 ? '9+' : unreadCount }}</span>
</div> </div>
<card-labels :conversation-id="chat.id" />
</div> </div>
<woot-context-menu <woot-context-menu
v-if="showContextMenu" v-if="showContextMenu"
@ -103,12 +102,10 @@
<conversation-context-menu <conversation-context-menu
:status="chat.status" :status="chat.status"
:inbox-id="inbox.id" :inbox-id="inbox.id"
:has-unread-messages="hasUnread"
@update-conversation="onUpdateConversation" @update-conversation="onUpdateConversation"
@assign-agent="onAssignAgent" @assign-agent="onAssignAgent"
@assign-label="onAssignLabel" @assign-label="onAssignLabel"
@assign-team="onAssignTeam" @assign-team="onAssignTeam"
@mark-as-unread="markAsUnread"
/> />
</woot-context-menu> </woot-context-menu>
</div> </div>
@ -126,8 +123,8 @@ import InboxName from '../InboxName';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
import ConversationContextMenu from './contextMenu/Index.vue'; import ConversationContextMenu from './contextMenu/Index.vue';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import TimeAgo from 'dashboard/components/ui/TimeAgo'; import timeAgo from 'dashboard/components/ui/TimeAgo';
import CardLabels from './conversationCardComponents/CardLabels.vue';
const ATTACHMENT_ICONS = { const ATTACHMENT_ICONS = {
image: 'image', image: 'image',
audio: 'headphones-sound-wave', audio: 'headphones-sound-wave',
@ -139,11 +136,10 @@ const ATTACHMENT_ICONS = {
export default { export default {
components: { components: {
CardLabels,
InboxName, InboxName,
Thumbnail, Thumbnail,
ConversationContextMenu, ConversationContextMenu,
TimeAgo, timeAgo,
}, },
mixins: [ mixins: [
@ -245,7 +241,7 @@ export default {
}, },
unreadCount() { unreadCount() {
return this.chat.unread_count; return this.unreadMessagesCount(this.chat);
}, },
hasUnread() { hasUnread() {
@ -363,24 +359,16 @@ export default {
this.$emit('assign-team', team, this.chat.id); this.$emit('assign-team', team, this.chat.id);
this.closeContextMenu(); this.closeContextMenu();
}, },
async markAsUnread() {
this.$emit('mark-as-unread', this.chat.id);
this.closeContextMenu();
},
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.conversation { .conversation {
align-items: flex-start; align-items: center;
&:hover { &:hover {
background: var(--color-background-light); background: var(--color-background-light);
} }
&::v-deep .user-thumbnail-box {
margin-top: var(--space-normal);
}
} }
.conversation-selected { .conversation-selected {
@ -389,10 +377,8 @@ export default {
.has-inbox-name { .has-inbox-name {
&::v-deep .user-thumbnail-box { &::v-deep .user-thumbnail-box {
margin-top: var(--space-large); margin-top: var(--space-normal);
} align-items: flex-start;
.checkbox-wrapper {
margin-top: var(--space-large);
} }
.conversation--meta { .conversation--meta {
margin-top: var(--space-normal); margin-top: var(--space-normal);
@ -437,7 +423,6 @@ export default {
margin-top: var(--space-minus-micro); margin-top: var(--space-minus-micro);
vertical-align: middle; vertical-align: middle;
} }
.checkbox-wrapper { .checkbox-wrapper {
height: 40px; height: 40px;
width: 40px; width: 40px;
@ -447,7 +432,6 @@ export default {
border-radius: 100%; border-radius: 100%;
margin-top: var(--space-normal); margin-top: var(--space-normal);
cursor: pointer; cursor: pointer;
&:hover { &:hover {
background-color: var(--w-100); background-color: var(--w-100);
} }

View file

@ -21,11 +21,7 @@
/> />
</h3> </h3>
<div class="conversation--header--actions"> <div class="conversation--header--actions">
<inbox-name <inbox-name :inbox="inbox" class="margin-right-small" />
v-if="hasMultipleInboxes"
:inbox="inbox"
class="margin-right-small"
/>
<span <span
v-if="isSnoozed" v-if="isSnoozed"
class="snoozed--display-text margin-right-small" class="snoozed--display-text margin-right-small"
@ -149,9 +145,6 @@ export default {
const { inbox_id: inboxId } = this.chat; const { inbox_id: inboxId } = this.chat;
return this.$store.getters['inboxes/getInbox'](inboxId); return this.$store.getters['inboxes/getInbox'](inboxId);
}, },
hasMultipleInboxes() {
return this.$store.getters['inboxes/getInboxes'].length > 1;
},
}, },
methods: { methods: {

View file

@ -1,6 +1,7 @@
<template> <template>
<li <li
v-if="hasAttachments || data.content || isEmailContentType" v-if="hasAttachments || data.content || isEmailContentType"
:id="'message' + data.id"
:class="alignBubble" :class="alignBubble"
> >
<div :class="wrapClass"> <div :class="wrapClass">
@ -15,6 +16,7 @@
v-if="data.content" v-if="data.content"
:message="message" :message="message"
:is-email="isEmailContentType" :is-email="isEmailContentType"
:readable-time="readableTime"
:display-quoted-button="displayQuotedButton" :display-quoted-button="displayQuotedButton"
/> />
<span <span
@ -28,6 +30,7 @@
<bubble-image <bubble-image
v-if="attachment.file_type === 'image' && !hasImageError" v-if="attachment.file_type === 'image' && !hasImageError"
:url="attachment.data_url" :url="attachment.data_url"
:readable-time="readableTime"
@error="onImageLoadError" @error="onImageLoadError"
/> />
<audio v-else-if="attachment.file_type === 'audio'" controls> <audio v-else-if="attachment.file_type === 'audio'" controls>
@ -36,6 +39,7 @@
<bubble-video <bubble-video
v-else-if="attachment.file_type === 'video'" v-else-if="attachment.file_type === 'video'"
:url="attachment.data_url" :url="attachment.data_url"
:readable-time="readableTime"
/> />
<bubble-location <bubble-location
v-else-if="attachment.file_type === 'location'" v-else-if="attachment.file_type === 'location'"
@ -43,7 +47,11 @@
:longitude="attachment.coordinates_long" :longitude="attachment.coordinates_long"
:name="attachment.fallback_title" :name="attachment.fallback_title"
/> />
<bubble-file v-else :url="attachment.data_url" /> <bubble-file
v-else
:url="attachment.data_url"
:readable-time="readableTime"
/>
</div> </div>
</div> </div>
<bubble-actions <bubble-actions
@ -52,15 +60,14 @@
:story-sender="storySender" :story-sender="storySender"
:story-id="storyId" :story-id="storyId"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory" :has-instagram-story="hasInstagramStory"
:is-email="isEmailContentType" :is-email="isEmailContentType"
:is-private="data.private" :is-private="data.private"
:message-type="data.message_type" :message-type="data.message_type"
:message-status="status" :readable-time="readableTime"
:source-id="data.source_id" :source-id="data.source_id"
:inbox-id="data.inbox_id" :inbox-id="data.inbox_id"
:created-at="createdAt" :message-read="showReadTicks"
/> />
</div> </div>
<spinner v-if="isPending" size="tiny" /> <spinner v-if="isPending" size="tiny" />
@ -98,8 +105,10 @@
<div v-if="shouldShowContextMenu" class="context-menu-wrap"> <div v-if="shouldShowContextMenu" class="context-menu-wrap">
<context-menu <context-menu
v-if="isBubble && !isMessageDeleted" v-if="isBubble && !isMessageDeleted"
:id="data.id"
:is-open="showContextMenu" :is-open="showContextMenu"
:show-copy="hasText" :show-copy="hasText"
:conversation-id="data.conversation_id"
:show-canned-response-option="isOutgoing" :show-canned-response-option="isOutgoing"
:menu-position="contextMenuPosition" :menu-position="contextMenuPosition"
:message-content="data.content" :message-content="data.content"
@ -111,6 +120,8 @@
</template> </template>
<script> <script>
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import timeMixin from '../../../mixins/time';
import BubbleMailHead from './bubble/MailHead'; import BubbleMailHead from './bubble/MailHead';
import BubbleText from './bubble/Text'; import BubbleText from './bubble/Text';
import BubbleImage from './bubble/Image'; import BubbleImage from './bubble/Image';
@ -139,7 +150,7 @@ export default {
ContextMenu, ContextMenu,
Spinner, Spinner,
}, },
mixins: [alertMixin, messageFormatterMixin, contentTypeMixin], mixins: [alertMixin, timeMixin, messageFormatterMixin, contentTypeMixin],
props: { props: {
data: { data: {
type: Object, type: Object,
@ -149,11 +160,11 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
isAWhatsAppChannel: { hasInstagramStory: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
hasInstagramStory: { hasUserReadMessage: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
@ -223,9 +234,6 @@ export default {
sender() { sender() {
return this.data.sender || {}; return this.data.sender || {};
}, },
status() {
return this.data.status;
},
storySender() { storySender() {
return this.contentAttributes.story_sender || null; return this.contentAttributes.story_sender || null;
}, },
@ -259,8 +267,11 @@ export default {
'has-tweet-menu': this.isATweet, 'has-tweet-menu': this.isATweet,
}; };
}, },
createdAt() { readableTime() {
return this.contentAttributes.external_created_at || this.data.created_at; return this.messageStamp(
this.contentAttributes.external_created_at || this.data.created_at,
'LLL d, h:mm a'
);
}, },
isBubble() { isBubble() {
return [0, 1, 3].includes(this.data.message_type); return [0, 1, 3].includes(this.data.message_type);
@ -271,6 +282,14 @@ export default {
isOutgoing() { isOutgoing() {
return this.data.message_type === MESSAGE_TYPE.OUTGOING; return this.data.message_type === MESSAGE_TYPE.OUTGOING;
}, },
showReadTicks() {
return (
(this.isOutgoing || this.isTemplate) &&
this.hasUserReadMessage &&
this.isWebWidgetInbox &&
!this.data.private
);
},
isTemplate() { isTemplate() {
return this.data.message_type === MESSAGE_TYPE.TEMPLATE; return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
}, },

View file

@ -35,18 +35,20 @@
<message <message
v-for="message in getReadMessages" v-for="message in getReadMessages"
:key="message.id" :key="message.id"
class="message--read ph-no-capture" class="message--read"
:data="message" :data="message"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory" :has-instagram-story="hasInstagramStory"
:has-user-read-message="
hasUserReadMessage(message.created_at, getLastSeenAt)
"
:is-web-widget-inbox="isAWebWidgetInbox" :is-web-widget-inbox="isAWebWidgetInbox"
/> />
<li v-show="unreadMessageCount != 0" class="unread--toast"> <li v-show="getUnreadCount != 0" class="unread--toast">
<span class="text-uppercase"> <span class="text-uppercase">
{{ unreadMessageCount }} {{ getUnreadCount }}
{{ {{
unreadMessageCount > 1 getUnreadCount > 1
? $t('CONVERSATION.UNREAD_MESSAGES') ? $t('CONVERSATION.UNREAD_MESSAGES')
: $t('CONVERSATION.UNREAD_MESSAGE') : $t('CONVERSATION.UNREAD_MESSAGE')
}} }}
@ -55,11 +57,13 @@
<message <message
v-for="message in getUnReadMessages" v-for="message in getUnReadMessages"
:key="message.id" :key="message.id"
class="message--unread ph-no-capture" class="message--unread"
:data="message" :data="message"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:is-a-whatsapp-channel="isAWhatsAppChannel"
:has-instagram-story="hasInstagramStory" :has-instagram-story="hasInstagramStory"
:has-user-read-message="
hasUserReadMessage(message.created_at, getLastSeenAt)
"
:is-web-widget-inbox="isAWebWidgetInbox" :is-web-widget-inbox="isAWebWidgetInbox"
/> />
</ul> </ul>
@ -133,6 +137,7 @@ export default {
allConversations: 'getAllConversations', allConversations: 'getAllConversations',
inboxesList: 'inboxes/getInboxes', inboxesList: 'inboxes/getInboxes',
listLoadingStatus: 'getAllMessagesLoaded', listLoadingStatus: 'getAllMessagesLoaded',
getUnreadCount: 'getUnreadCount',
loadingChatList: 'getChatListLoadingStatus', loadingChatList: 'getChatListLoadingStatus',
}), }),
inboxId() { inboxId() {
@ -266,9 +271,6 @@ export default {
} }
return ''; return '';
}, },
unreadMessageCount() {
return this.currentChat.unread_count;
},
}, },
watch: { watch: {
@ -302,8 +304,15 @@ export default {
setSelectedTweet(tweetId) { setSelectedTweet(tweetId) {
this.selectedTweetId = tweetId; this.selectedTweetId = tweetId;
}, },
onScrollToMessage() { onScrollToMessage({ messageId = '' } = {}) {
this.$nextTick(() => this.scrollToBottom()); this.$nextTick(() => {
const messageElement = document.getElementById('message' + messageId);
if (messageElement) {
messageElement.scrollIntoView({ behavior: 'smooth' });
} else {
this.scrollToBottom();
}
});
this.makeMessagesRead(); this.makeMessagesRead();
}, },
showPopoutReplyBox() { showPopoutReplyBox() {
@ -329,7 +338,7 @@ export default {
}, },
scrollToBottom() { scrollToBottom() {
let relevantMessages = []; let relevantMessages = [];
if (this.unreadMessageCount > 0) { if (this.getUnreadCount > 0) {
// capturing only the unread messages // capturing only the unread messages
relevantMessages = this.conversationPanel.querySelectorAll( relevantMessages = this.conversationPanel.querySelectorAll(
'.message--unread' '.message--unread'
@ -427,7 +436,12 @@ export default {
position: fixed; position: fixed;
left: unset; left: unset;
position: absolute; position: absolute;
bottom: var(--space-smaller);
&::before {
transform: rotate(0deg);
left: var(--space-smaller);
bottom: var(--space-minus-slab);
}
} }
} }
} }

View file

@ -37,7 +37,6 @@
<woot-audio-recorder <woot-audio-recorder
v-if="showAudioRecorderEditor" v-if="showAudioRecorderEditor"
ref="audioRecorderInput" ref="audioRecorderInput"
:audio-record-format="audioRecordFormat"
@state-recorder-progress-changed="onStateProgressRecorderChanged" @state-recorder-progress-changed="onStateProgressRecorderChanged"
@state-recorder-changed="onStateRecorderChanged" @state-recorder-changed="onStateRecorderChanged"
@finish-record="onFinishRecorder" @finish-record="onFinishRecorder"
@ -61,7 +60,6 @@
class="input" class="input"
:is-private="isOnPrivateNote" :is-private="isOnPrivateNote"
:placeholder="messagePlaceHolder" :placeholder="messagePlaceHolder"
:update-selection-with="updateEditorSelectionWith"
:min-height="4" :min-height="4"
@typing-off="onTypingOff" @typing-off="onTypingOff"
@typing-on="onTypingOn" @typing-on="onTypingOn"
@ -69,7 +67,6 @@
@blur="onBlur" @blur="onBlur"
@toggle-user-mention="toggleUserMention" @toggle-user-mention="toggleUserMention"
@toggle-canned-menu="toggleCannedMenu" @toggle-canned-menu="toggleCannedMenu"
@clear-selection="clearEditorSelection"
/> />
</div> </div>
<div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste"> <div v-if="hasAttachments" class="attachment-preview-box" @paste="onPaste">
@ -133,6 +130,7 @@ import { mapGetters } from 'vuex';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import alertMixin from 'shared/mixins/alertMixin'; import alertMixin from 'shared/mixins/alertMixin';
import EmojiInput from 'shared/components/emoji/EmojiInput';
import CannedResponse from './CannedResponse'; import CannedResponse from './CannedResponse';
import ResizableTextArea from 'shared/components/ResizableTextArea'; import ResizableTextArea from 'shared/components/ResizableTextArea';
import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview'; import AttachmentPreview from 'dashboard/components/widgets/AttachmentsPreview';
@ -148,7 +146,6 @@ import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { import {
MAXIMUM_FILE_UPLOAD_SIZE, MAXIMUM_FILE_UPLOAD_SIZE,
MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL, MAXIMUM_FILE_UPLOAD_SIZE_TWILIO_SMS_CHANNEL,
AUDIO_FORMATS,
} from 'shared/constants/messages'; } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
@ -163,11 +160,6 @@ import { LocalStorage, LOCAL_STORAGE_KEYS } from '../../../helper/localStorage';
import { trimContent, debounce } from '@chatwoot/utils'; import { trimContent, debounce } from '@chatwoot/utils';
import wootConstants from 'dashboard/constants'; import wootConstants from 'dashboard/constants';
import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings'; import { isEditorHotKeyEnabled } from 'dashboard/mixins/uiSettings';
import AnalyticsHelper, {
ANALYTICS_EVENTS,
} from '../../../helper/AnalyticsHelper';
const EmojiInput = () => import('shared/components/emoji/EmojiInput');
export default { export default {
components: { components: {
@ -223,7 +215,6 @@ export default {
ccEmails: '', ccEmails: '',
doAutoSaveDraft: () => {}, doAutoSaveDraft: () => {},
showWhatsAppTemplatesModal: false, showWhatsAppTemplatesModal: false,
updateEditorSelectionWith: '',
}; };
}, },
computed: { computed: {
@ -407,7 +398,7 @@ export default {
return conversationDisplayType !== CONDENSED; return conversationDisplayType !== CONDENSED;
}, },
emojiDialogClassOnExpanedLayout() { emojiDialogClassOnExpanedLayout() {
return this.isOnExpandedLayout || this.popoutReplyBox return this.isOnExpandedLayout && !this.popoutReplyBox
? 'emoji-dialog--expanded' ? 'emoji-dialog--expanded'
: ''; : '';
}, },
@ -459,17 +450,12 @@ export default {
return this.currentChat.id; return this.currentChat.id;
}, },
conversationIdByRoute() { conversationIdByRoute() {
return this.conversationId; const { conversation_id: conversationId } = this.$route.params;
return conversationId;
}, },
editorStateId() { editorStateId() {
return `draft-${this.conversationIdByRoute}-${this.replyType}`; return `draft-${this.conversationIdByRoute}-${this.replyType}`;
}, },
audioRecordFormat() {
if (this.isAWebWidgetInbox) {
return AUDIO_FORMATS.WEBM;
}
return AUDIO_FORMATS.OGG;
},
}, },
watch: { watch: {
currentChat(conversation) { currentChat(conversation) {
@ -601,7 +587,6 @@ export default {
e.preventDefault(); e.preventDefault();
} else if (keyCode === 'enter' && this.isAValidEvent('enter')) { } else if (keyCode === 'enter' && this.isAValidEvent('enter')) {
this.onSendReply(); this.onSendReply();
e.preventDefault();
} else if ( } else if (
['meta+enter', 'ctrl+enter'].includes(keyCode) && ['meta+enter', 'ctrl+enter'].includes(keyCode) &&
this.isAValidEvent('cmd_enter') this.isAValidEvent('cmd_enter')
@ -709,7 +694,6 @@ export default {
}, },
replaceText(message) { replaceText(message) {
setTimeout(() => { setTimeout(() => {
AnalyticsHelper.track(ANALYTICS_EVENTS.INSERTED_A_CANNED_RESPONSE);
this.message = message; this.message = message;
}, 100); }, 100);
}, },
@ -724,26 +708,8 @@ export default {
} }
this.$nextTick(() => this.$refs.messageInput.focus()); this.$nextTick(() => this.$refs.messageInput.focus());
}, },
clearEditorSelection() {
this.updateEditorSelectionWith = '';
},
insertEmoji(emoji, selectionStart, selectionEnd) {
const { message } = this;
const newMessage =
message.slice(0, selectionStart) +
emoji +
message.slice(selectionEnd, message.length);
this.message = newMessage;
},
emojiOnClick(emoji) { emojiOnClick(emoji) {
if (this.showRichContentEditor) { this.message = `${this.message}${emoji} `;
this.updateEditorSelectionWith = emoji;
this.onFocus();
}
if (!this.showRichContentEditor) {
const { selectionStart, selectionEnd } = this.$refs.messageInput.$el;
this.insertEmoji(emoji, selectionStart, selectionEnd);
}
}, },
clearMessage() { clearMessage() {
this.message = ''; this.message = '';
@ -998,13 +964,13 @@ export default {
.emoji-dialog { .emoji-dialog {
top: unset; top: unset;
bottom: var(--space-normal); bottom: 12px;
left: -320px; left: -320px;
right: unset; right: unset;
&::before { &::before {
right: var(--space-minus-normal); right: -16px;
bottom: var(--space-small); bottom: 10px;
transform: rotate(270deg); transform: rotate(270deg);
filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08)); filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.08));
} }
@ -1018,7 +984,7 @@ export default {
&::before { &::before {
transform: rotate(0deg); transform: rotate(0deg);
left: var(--space-smaller); left: var(--space-smaller);
bottom: var(--space-minus-small); bottom: var(--space-minus-slab);
} }
} }
.message-signature { .message-signature {

View file

@ -1,38 +1,22 @@
<template> <template>
<div class="message-text--metadata"> <div class="message-text--metadata">
<span <span class="time" :class="{ delivered: messageRead }">{{
class="time" readableTime
:class="{ }}</span>
'has-status-icon': <span v-if="showSentIndicator" class="time">
showSentIndicator || showDeliveredIndicator || showReadIndicator,
}"
>
{{ readableTime }}
</span>
<span v-if="showReadIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
icon="checkmark-double"
class="action--icon read-tick read-indicator"
size="14"
/>
</span>
<span v-else-if="showDeliveredIndicator" class="read-indicator-wrap">
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.DELIVERED')"
icon="checkmark-double"
class="action--icon read-tick"
size="14"
/>
</span>
<span v-else-if="showSentIndicator" class="read-indicator-wrap">
<fluent-icon <fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.SENT')" v-tooltip.top-start="$t('CHAT_LIST.SENT')"
icon="checkmark" icon="checkmark"
class="action--icon read-tick"
size="14" size="14"
/> />
</span> </span>
<fluent-icon
v-if="messageRead"
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
icon="checkmark-double"
class="action--icon read-tick"
size="12"
/>
<fluent-icon <fluent-icon
v-if="isEmail" v-if="isEmail"
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')" v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
@ -60,6 +44,19 @@
size="16" size="16"
/> />
</button> </button>
<a
v-if="hasInstagramStory && (isIncoming || isOutgoing) && linkToStory"
:href="linkToStory"
target="_blank"
rel="noopener noreferrer nofollow"
>
<fluent-icon
v-tooltip.top-start="$t('CHAT_LIST.LINK_TO_STORY')"
icon="open"
class="action--icon cursor-pointer"
size="16"
/>
</a>
<a <a
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet" v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
:href="linkToTweet" :href="linkToTweet"
@ -77,22 +74,20 @@
</template> </template>
<script> <script>
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages'; import { MESSAGE_TYPE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time';
export default { export default {
mixins: [inboxMixin, timeMixin], mixins: [inboxMixin],
props: { props: {
sender: { sender: {
type: Object, type: Object,
default: () => ({}), default: () => ({}),
}, },
createdAt: { readableTime: {
type: Number, type: String,
default: 0, default: '',
}, },
storySender: { storySender: {
type: String, type: String,
@ -122,10 +117,6 @@ export default {
type: Number, type: Number,
default: 1, default: 1,
}, },
messageStatus: {
type: String,
default: '',
},
sourceId: { sourceId: {
type: String, type: String,
default: '', default: '',
@ -138,9 +129,12 @@ export default {
type: [String, Number], type: [String, Number],
default: 0, default: 0,
}, },
messageRead: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
...mapGetters({ currentChat: 'getSelectedChat' }),
inbox() { inbox() {
return this.$store.getters['inboxes/getInbox'](this.inboxId); return this.$store.getters['inboxes/getInbox'](this.inboxId);
}, },
@ -150,21 +144,6 @@ export default {
isOutgoing() { isOutgoing() {
return MESSAGE_TYPE.OUTGOING === this.messageType; return MESSAGE_TYPE.OUTGOING === this.messageType;
}, },
isTemplate() {
return MESSAGE_TYPE.TEMPLATE === this.messageType;
},
isDelivered() {
return MESSAGE_STATUS.DELIVERED === this.messageStatus;
},
isRead() {
return MESSAGE_STATUS.READ === this.messageStatus;
},
isSent() {
return MESSAGE_STATUS.SENT === this.messageStatus;
},
readableTime() {
return this.messageStamp(this.createdAt, 'LLL d, h:mm a');
},
screenName() { screenName() {
const { additional_attributes: additionalAttributes = {} } = const { additional_attributes: additionalAttributes = {} } =
this.sender || {}; this.sender || {};
@ -185,52 +164,12 @@ export default {
const { storySender, storyId } = this; const { storySender, storyId } = this;
return `https://www.instagram.com/stories/${storySender}/${storyId}`; return `https://www.instagram.com/stories/${storySender}/${storyId}`;
}, },
showStatusIndicators() {
if ((this.isOutgoing || this.isTemplate) && !this.private) {
return true;
}
return false;
},
showSentIndicator() { showSentIndicator() {
if (!this.showStatusIndicators) { return (
return false; this.isOutgoing &&
} this.sourceId &&
(this.isAnEmailChannel || this.isAWhatsAppChannel)
if (this.isAnEmailChannel) { );
return !!this.sourceId;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isSent;
}
return false;
},
showDeliveredIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isDelivered;
}
return false;
},
showReadIndicator() {
if (!this.showStatusIndicators) {
return false;
}
if (this.isAWebWidgetInbox) {
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt >= this.createdAt;
}
if (this.isAWhatsAppChannel) {
return this.sourceId && this.isRead;
}
return false;
}, },
}, },
methods: { methods: {
@ -246,21 +185,16 @@ export default {
.right { .right {
.message-text--metadata { .message-text--metadata {
align-items: center;
.time { .time {
color: var(--w-100); color: var(--w-100);
} }
.action--icon { .action--icon {
color: var(--white);
&.read-tick { &.read-tick {
color: var(--v-100); color: var(--v-100);
margin-top: calc(var(--space-micro) + var(--space-micro) / 2);
} }
color: var(--white);
&.read-indicator {
color: var(--g-200);
}
} }
.lock--icon--private { .lock--icon--private {
@ -324,9 +258,8 @@ export default {
position: absolute; position: absolute;
right: var(--space-small); right: var(--space-small);
white-space: nowrap; white-space: nowrap;
&.delivered {
&.has-status-icon { right: var(--space-medium);
right: var(--space-large);
line-height: 2; line-height: 2;
} }
} }
@ -363,10 +296,4 @@ export default {
.delivered-icon { .delivered-icon {
margin-left: -var(--space-normal); margin-left: -var(--space-normal);
} }
.read-indicator-wrap {
line-height: 1;
display: flex;
align-items: center;
}
</style> </style>

View file

@ -35,6 +35,10 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
readableTime: {
type: String,
default: '',
},
isEmail: { isEmail: {
type: Boolean, type: Boolean,
default: true, default: true,

View file

@ -1,11 +1,5 @@
<template> <template>
<div class="menu-container"> <div class="menu-container">
<menu-item
v-if="!hasUnreadMessages"
:option="unreadOption"
variant="icon"
@click="$emit('mark-as-unread')"
/>
<template v-for="option in statusMenuConfig"> <template v-for="option in statusMenuConfig">
<menu-item <menu-item
v-if="show(option.key)" v-if="show(option.key)"
@ -85,10 +79,6 @@ export default {
type: String, type: String,
default: '', default: '',
}, },
hasUnreadMessages: {
type: Boolean,
default: false,
},
inboxId: { inboxId: {
type: Number, type: Number,
default: null, default: null,
@ -97,10 +87,6 @@ export default {
data() { data() {
return { return {
STATUS_TYPE: wootConstants.STATUS_TYPE, STATUS_TYPE: wootConstants.STATUS_TYPE,
unreadOption: {
label: this.$t('CONVERSATION.CARD_CONTEXT_MENU.MARK_AS_UNREAD'),
icon: 'mail',
},
statusMenuConfig: [ statusMenuConfig: [
{ {
key: wootConstants.STATUS_TYPE.RESOLVED, key: wootConstants.STATUS_TYPE.RESOLVED,

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="bulk-action__agents"> <div class="bulk-action__agents">
<div class="triangle" :style="cssVars"> <div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24"> <svg height="12" viewBox="0 0 24 12" width="24">
<path <path
d="M20 12l-8-8-12 12" d="M20 12l-8-8-12 12"
@ -105,14 +105,13 @@ import { mapGetters } from 'vuex';
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
import Spinner from 'shared/components/Spinner'; import Spinner from 'shared/components/Spinner';
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default { export default {
components: { components: {
Thumbnail, Thumbnail,
Spinner, Spinner,
}, },
mixins: [clickaway, bulkActionsMixin], mixins: [clickaway],
props: { props: {
selectedInboxes: { selectedInboxes: {
type: Array, type: Array,
@ -234,7 +233,7 @@ export default {
z-index: var(--z-index-one); z-index: var(--z-index-one);
position: absolute; position: absolute;
top: calc(var(--space-slab) * -1); top: calc(var(--space-slab) * -1);
right: var(--triangle-position); right: var(--space-micro);
text-align: left; text-align: left;
} }
} }

View file

@ -43,26 +43,25 @@
variant="smooth" variant="smooth"
color-scheme="secondary" color-scheme="secondary"
icon="person-assign" icon="person-assign"
class="margin-right-smaller"
@click="toggleAgentList" @click="toggleAgentList"
/> />
<woot-button
v-tooltip="$t('BULK_ACTION.ASSIGN_TEAM_TOOLTIP')"
size="tiny"
variant="smooth"
color-scheme="secondary"
icon="people-team-add"
@click="toggleTeamsList"
/>
</div> </div>
<transition name="popover-animation"> <transition name="popover-animation">
<label-actions <label-actions
v-if="showLabelActions" v-if="showLabelActions"
triangle-position="8.5"
@assign="assignLabels" @assign="assignLabels"
@close="showLabelActions = false" @close="showLabelActions = false"
/> />
</transition> </transition>
<transition name="popover-animation">
<agent-selector
v-if="showAgentsList"
:selected-inboxes="selectedInboxes"
:conversation-count="conversations.length"
@select="submit"
@close="showAgentsList = false"
/>
</transition>
<transition name="popover-animation"> <transition name="popover-animation">
<update-actions <update-actions
v-if="showUpdateActions" v-if="showUpdateActions"
@ -71,29 +70,10 @@
:show-resolve="!showResolvedAction" :show-resolve="!showResolvedAction"
:show-reopen="!showOpenAction" :show-reopen="!showOpenAction"
:show-snooze="!showSnoozedAction" :show-snooze="!showSnoozedAction"
triangle-position="5.6"
@update="updateConversations" @update="updateConversations"
@close="showUpdateActions = false" @close="showUpdateActions = false"
/> />
</transition> </transition>
<transition name="popover-animation">
<agent-selector
v-if="showAgentsList"
:selected-inboxes="selectedInboxes"
:conversation-count="conversations.length"
triangle-position="2.8"
@select="submit"
@close="showAgentsList = false"
/>
</transition>
<transition name="popover-animation">
<team-actions
v-if="showTeamsList"
triangle-position="0.2"
@assign-team="assignTeam"
@close="showTeamsList = false"
/>
</transition>
</div> </div>
<div v-if="allConversationsSelected" class="bulk-action__alert"> <div v-if="allConversationsSelected" class="bulk-action__alert">
{{ $t('BULK_ACTION.ALL_CONVERSATIONS_SELECTED_ALERT') }} {{ $t('BULK_ACTION.ALL_CONVERSATIONS_SELECTED_ALERT') }}
@ -105,13 +85,11 @@
import AgentSelector from './AgentSelector.vue'; import AgentSelector from './AgentSelector.vue';
import UpdateActions from './UpdateActions.vue'; import UpdateActions from './UpdateActions.vue';
import LabelActions from './LabelActions.vue'; import LabelActions from './LabelActions.vue';
import TeamActions from './TeamActions.vue';
export default { export default {
components: { components: {
AgentSelector, AgentSelector,
UpdateActions, UpdateActions,
LabelActions, LabelActions,
TeamActions,
}, },
props: { props: {
conversations: { conversations: {
@ -144,8 +122,6 @@ export default {
showAgentsList: false, showAgentsList: false,
showUpdateActions: false, showUpdateActions: false,
showLabelActions: false, showLabelActions: false,
showTeamsList: false,
popoverPositions: {},
}; };
}, },
methods: { methods: {
@ -161,9 +137,6 @@ export default {
assignLabels(labels) { assignLabels(labels) {
this.$emit('assign-labels', labels); this.$emit('assign-labels', labels);
}, },
assignTeam(team) {
this.$emit('assign-team', team);
},
resolveConversations() { resolveConversations() {
this.$emit('resolve-conversations'); this.$emit('resolve-conversations');
}, },
@ -176,9 +149,6 @@ export default {
toggleAgentList() { toggleAgentList() {
this.showAgentsList = !this.showAgentsList; this.showAgentsList = !this.showAgentsList;
}, },
toggleTeamsList() {
this.showTeamsList = !this.showTeamsList;
},
}, },
}; };
</script> </script>

View file

@ -1,6 +1,6 @@
<template> <template>
<div v-on-clickaway="onClose" class="labels-container"> <div v-on-clickaway="onClose" class="labels-container">
<div class="triangle" :style="cssVars"> <div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24"> <svg height="12" viewBox="0 0 24 12" width="24">
<path <path
d="M20 12l-8-8-12 12" d="M20 12l-8-8-12 12"
@ -75,10 +75,9 @@
<script> <script>
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default { export default {
mixins: [clickaway, bulkActionsMixin], mixins: [clickaway],
data() { data() {
return { return {
query: '', query: '',
@ -161,7 +160,7 @@ export default {
max-width: var(--space-giga); max-width: var(--space-giga);
min-width: var(--space-giga); min-width: var(--space-giga);
position: absolute; position: absolute;
right: var(--space-small); right: 4.5rem;
top: var(--space-larger); top: var(--space-larger);
transform-origin: top right; transform-origin: top right;
width: auto; width: auto;
@ -205,7 +204,7 @@ export default {
.triangle { .triangle {
display: block; display: block;
position: absolute; position: absolute;
right: var(--triangle-position); right: var(--space-two);
text-align: left; text-align: left;
top: calc(var(--space-slab) * -1); top: calc(var(--space-slab) * -1);
z-index: var(--z-index-one); z-index: var(--z-index-one);

View file

@ -1,174 +0,0 @@
<template>
<div v-on-clickaway="onClose" class="bulk-action__teams">
<div class="triangle" :style="cssVars">
<svg height="12" viewBox="0 0 24 12" width="24">
<path
d="M20 12l-8-8-12 12"
fill="var(--white)"
fill-rule="evenodd"
stroke="var(--s-50)"
stroke-width="1px"
/>
</svg>
</div>
<div class="header flex-between">
<span>{{ $t('BULK_ACTION.TEAMS.TEAM_SELECT_LABEL') }}</span>
<woot-button
size="tiny"
variant="clear"
color-scheme="secondary"
icon="dismiss"
@click="onClose"
/>
</div>
<div class="container">
<div class="team__list-container">
<ul>
<li class="search-container">
<div class="agent-list-search flex-between">
<fluent-icon icon="search" class="search-icon" size="16" />
<input
ref="search"
v-model="query"
type="search"
placeholder="Search"
class="agent--search_input"
/>
</div>
</li>
<template v-if="filteredTeams.length">
<li v-for="team in filteredTeams" :key="team.id">
<div class="team__list-item" @click="assignTeam(team)">
<span class="reports-option__title">{{ team.name }}</span>
</div>
</li>
</template>
<li v-else>
<div class="team__list-item">
<span class="reports-option__title">{{
$t('BULK_ACTION.TEAMS.NO_TEAMS_AVAILABLE')
}}</span>
</div>
</li>
</ul>
</div>
</div>
</div>
</template>
<script>
import { mixin as clickaway } from 'vue-clickaway';
import { mapGetters } from 'vuex';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default {
mixins: [clickaway, bulkActionsMixin],
data() {
return {
query: '',
selectedteams: [],
};
},
computed: {
...mapGetters({ teams: 'teams/getTeams' }),
filteredTeams() {
return [
{ name: 'None', id: 0 },
...this.teams.filter(team =>
team.name.toLowerCase().includes(this.query.toLowerCase())
),
];
},
},
methods: {
assignTeam(key) {
this.$emit('assign-team', key);
},
onClose() {
this.$emit('close');
},
},
};
</script>
<style scoped lang="scss">
.bulk-action__teams {
background-color: var(--white);
border-radius: var(--border-radius-large);
border: 1px solid var(--s-50);
box-shadow: var(--shadow-dropdown-pane);
max-width: 75%;
position: absolute;
right: var(--space-small);
top: var(--space-larger);
transform-origin: top right;
width: auto;
z-index: var(--z-index-twenty);
min-width: var(--space-giga);
.header {
padding: var(--space-one);
span {
font-size: var(--font-size-small);
font-weight: var(--font-weight-medium);
}
}
.container {
max-height: var(--space-giga);
overflow-y: auto;
.team__list-container {
height: 100%;
}
.agent-list-search {
padding: 0 var(--space-one);
border: 1px solid var(--s-100);
border-radius: var(--border-radius-medium);
background-color: var(--s-50);
.search-icon {
color: var(--s-400);
}
.agent--search_input {
border: 0;
font-size: var(--font-size-mini);
margin: 0;
background-color: transparent;
height: unset;
}
}
}
.triangle {
display: block;
z-index: var(--z-index-one);
position: absolute;
top: calc(var(--space-slab) * -1);
right: var(--triangle-position);
text-align: left;
}
}
ul {
margin: 0;
list-style: none;
}
.team__list-item {
display: flex;
align-items: center;
padding: var(--space-one);
cursor: pointer;
&:hover {
background-color: var(--s-50);
}
span {
font-size: var(--font-size-small);
}
}
.search-container {
padding: 0 var(--space-one);
position: sticky;
top: 0;
z-index: var(--z-index-twenty);
background-color: var(--white);
}
</style>

View file

@ -1,6 +1,6 @@
<template> <template>
<div v-on-clickaway="onClose" class="actions-container"> <div v-on-clickaway="onClose" class="actions-container">
<div class="triangle" :style="cssVars"> <div class="triangle">
<svg height="12" viewBox="0 0 24 12" width="24"> <svg height="12" viewBox="0 0 24 12" width="24">
<path <path
d="M20 12l-8-8-12 12" d="M20 12l-8-8-12 12"
@ -45,14 +45,12 @@
import { mixin as clickaway } from 'vue-clickaway'; import { mixin as clickaway } from 'vue-clickaway';
import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue'; import WootDropdownItem from 'shared/components/ui/dropdown/DropdownItem.vue';
import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue'; import WootDropdownMenu from 'shared/components/ui/dropdown/DropdownMenu.vue';
import bulkActionsMixin from 'dashboard/mixins/bulkActionsMixin.js';
export default { export default {
components: { components: {
WootDropdownItem, WootDropdownItem,
WootDropdownMenu, WootDropdownMenu,
}, },
mixins: [clickaway, bulkActionsMixin], mixins: [clickaway],
props: { props: {
selectedInboxes: { selectedInboxes: {
type: Array, type: Array,
@ -133,7 +131,7 @@ export default {
box-shadow: var(--shadow-dropdown-pane); box-shadow: var(--shadow-dropdown-pane);
position: absolute; position: absolute;
right: var(--space-small); right: var(--space-small);
top: var(--space-larger); top: 48px;
transform-origin: top right; transform-origin: top right;
width: auto; width: auto;
z-index: var(--z-index-twenty); z-index: var(--z-index-twenty);
@ -154,7 +152,7 @@ export default {
.triangle { .triangle {
display: block; display: block;
position: absolute; position: absolute;
right: var(--triangle-position); right: 2.8rem;
text-align: left; text-align: left;
top: calc(var(--space-slab) * -1); top: calc(var(--space-slab) * -1);
z-index: var(--z-index-one); z-index: var(--z-index-one);

View file

@ -1,136 +0,0 @@
<template>
<div
v-show="activeLabels.length"
ref="labelContainer"
class="label-container"
>
<div class="labels-wrap" :class="{ expand: showAllLabels }">
<woot-label
v-for="(label, index) in activeLabels"
:key="label.id"
:title="label.title"
:description="label.description"
:color="label.color"
variant="smooth"
small
:class="{ hidden: !showAllLabels && index > labelPosition }"
/>
<woot-button
v-if="showExpandLabelButton"
:title="
showAllLabels
? $t('CONVERSATION.CARD.HIDE_LABELS')
: $t('CONVERSATION.CARD.SHOW_LABELS')
"
class="show-more--button"
color-scheme="secondary"
variant="hollow"
:icon="showAllLabels ? 'chevron-left' : 'chevron-right'"
size="tiny"
@click="onShowLabels"
/>
</div>
</div>
</template>
<script>
import conversationLabelMixin from 'dashboard/mixins/conversation/labelMixin';
export default {
mixins: [conversationLabelMixin],
props: {
conversationId: {
type: Number,
required: true,
},
},
data() {
return {
showAllLabels: false,
showExpandLabelButton: false,
labelPosition: -1,
};
},
watch: {
activeLabels() {
this.$nextTick(() => this.computeVisibleLabelPosition());
},
},
mounted() {
this.computeVisibleLabelPosition();
},
methods: {
onShowLabels(e) {
e.stopPropagation();
this.showAllLabels = !this.showAllLabels;
},
computeVisibleLabelPosition() {
const labelContainer = this.$refs.labelContainer;
const labels = this.$refs.labelContainer.querySelectorAll('.label');
let labelOffset = 0;
this.showExpandLabelButton = false;
Array.from(labels).forEach((label, index) => {
labelOffset += label.offsetWidth + 8;
if (labelOffset < labelContainer.clientWidth - 16) {
this.labelPosition = index;
} else {
this.showExpandLabelButton = true;
}
});
},
},
};
</script>
<style lang="scss" scoped>
.show-more--button {
height: var(--space-two);
position: sticky;
flex-shrink: 0;
right: 0;
margin-right: var(--space-medium);
&.secondary:focus {
color: var(--s-700);
border-color: var(--s-300);
}
}
.label-container {
margin-top: var(--space-micro);
}
.labels-wrap {
display: flex;
align-items: center;
min-width: 0;
flex-shrink: 1;
&.expand {
height: auto;
overflow: visible;
flex-flow: row wrap;
.label {
margin-bottom: var(--space-smaller);
}
.show-more--button {
margin-bottom: var(--space-smaller);
}
}
.secondary {
border: 1px solid var(--s-100);
}
.label {
margin-bottom: 0;
}
}
.hidden {
visibility: hidden;
position: absolute;
}
</style>

View file

@ -22,6 +22,5 @@ export default {
EXPANDED: 'expanded', EXPANDED: 'expanded',
}, },
DOCS_URL: '//www.chatwoot.com/docs/product/', DOCS_URL: '//www.chatwoot.com/docs/product/',
TESTIMONIAL_URL: 'https://testimonials.cdn.chatwoot.com/content.json',
}; };
export const DEFAULT_REDIRECT_URL = '/app/'; export const DEFAULT_REDIRECT_URL = '/app/';

View file

@ -1,18 +1,13 @@
export const FEATURE_FLAGS = { export const FEATURE_FLAGS = {
AGENT_BOTS: 'agent_bots', AGENT_BOTS: 'agent_bots',
AGENT_MANAGEMENT: 'agent_management', AGENT_MANAGEMENT: 'agent_management',
AUTO_RESOLVE_CONVERSATIONS: 'auto_resolve_conversations',
AUTOMATIONS: 'automations', AUTOMATIONS: 'automations',
CAMPAIGNS: 'campaigns',
CANNED_RESPONSES: 'canned_responses', CANNED_RESPONSES: 'canned_responses',
CRM: 'crm',
CUSTOM_ATTRIBUTES: 'custom_attributes', CUSTOM_ATTRIBUTES: 'custom_attributes',
INBOX_MANAGEMENT: 'inbox_management', INBOX_MANAGEMENT: 'inbox_management',
INTEGRATIONS: 'integrations', INTEGRATIONS: 'integrations',
LABELS: 'labels', LABELS: 'labels',
MACROS: 'macros', MACROS: 'macros',
HELP_CENTER: 'help_center',
REPORTS: 'reports',
TEAM_MANAGEMENT: 'team_management', TEAM_MANAGEMENT: 'team_management',
VOICE_RECORDER: 'voice_recorder', VOICE_RECORDER: 'voice_recorder',
}; };

View file

@ -1,9 +0,0 @@
export const EXECUTED_A_MACRO = 'Executed a macro';
export const SENT_MESSAGE = 'Sent a message';
export const SENT_PRIVATE_NOTE = 'Sent a private note';
export const INSERTED_A_CANNED_RESPONSE = 'Inserted a canned response';
export const USED_MENTIONS = 'Used mentions';
export const MERGED_CONTACTS = 'Used merge contact option';
export const ADDED_TO_CANNED_RESPONSE = 'Used added to canned response option';
export const ADDED_A_CUSTOM_ATTRIBUTE = 'Added a custom attribute';
export const ADDED_AN_INBOX = 'Added an inbox';

View file

@ -1,67 +0,0 @@
import { AnalyticsBrowser } from '@june-so/analytics-next';
class AnalyticsHelper {
constructor({ token: analyticsToken } = {}) {
this.analyticsToken = analyticsToken;
this.analytics = null;
this.user = {};
}
async init() {
if (!this.analyticsToken) {
return;
}
let [analytics] = await AnalyticsBrowser.load({
writeKey: this.analyticsToken,
});
this.analytics = analytics;
}
identify(user) {
if (!this.analytics) {
return;
}
this.user = user;
this.analytics.identify(this.user.email, {
userId: this.user.id,
email: this.user.email,
name: this.user.name,
avatar: this.user.avatar_url,
});
const { accounts, account_id: accountId } = this.user;
const [currentAccount] = accounts.filter(
account => account.id === accountId
);
if (currentAccount) {
this.analytics.group(currentAccount.id, this.user.id, {
name: currentAccount.name,
});
}
}
track(eventName, properties = {}) {
if (!this.analytics) {
return;
}
this.analytics.track({
userId: this.user.id,
event: eventName,
properties,
});
}
page(params) {
if (!this.analytics) {
return;
}
this.analytics.page(params);
}
}
export * as ANALYTICS_EVENTS from './events';
export default new AnalyticsHelper(window.analyticsConfig);

View file

@ -56,8 +56,6 @@ export const conversationUrl = ({
url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`; url = `accounts/${accountId}/custom_view/${foldersId}/conversations/${id}`;
} else if (conversationType === 'mention') { } else if (conversationType === 'mention') {
url = `accounts/${accountId}/mentions/conversations/${id}`; url = `accounts/${accountId}/mentions/conversations/${id}`;
} else if (conversationType === 'unattended') {
url = `accounts/${accountId}/unattended/conversations/${id}`;
} }
return url; return url;
}; };
@ -68,23 +66,16 @@ export const conversationListPageURL = ({
inboxId, inboxId,
label, label,
teamId, teamId,
customViewId,
}) => { }) => {
let url = `accounts/${accountId}/dashboard`; let url = `accounts/${accountId}/dashboard`;
if (label) { if (label) {
url = `accounts/${accountId}/label/${label}`; url = `accounts/${accountId}/label/${label}`;
} else if (teamId) { } else if (teamId) {
url = `accounts/${accountId}/team/${teamId}`; url = `accounts/${accountId}/team/${teamId}`;
} else if (conversationType === 'mention') {
url = `accounts/${accountId}/mentions/conversations`;
} else if (inboxId) { } else if (inboxId) {
url = `accounts/${accountId}/inbox/${inboxId}`; url = `accounts/${accountId}/inbox/${inboxId}`;
} else if (customViewId) {
url = `accounts/${accountId}/custom_view/${customViewId}`;
} else if (conversationType) {
const urlMap = {
mention: 'mentions/conversations',
unattended: 'unattended/conversations',
};
url = `accounts/${accountId}/${urlMap[conversationType]}`;
} }
return frontendURL(url); return frontendURL(url);
}; };

View file

@ -17,22 +17,13 @@ const formatArray = params => {
return params; return params;
}; };
const generatePayloadForObject = item => {
if (item.action_params.id) {
item.action_params = [item.action_params.id];
} else {
item.action_params = [item.action_params];
}
return item.action_params;
};
const generatePayload = data => { const generatePayload = data => {
const actions = JSON.parse(JSON.stringify(data)); const actions = JSON.parse(JSON.stringify(data));
let payload = actions.map(item => { let payload = actions.map(item => {
if (Array.isArray(item.action_params)) { if (Array.isArray(item.action_params)) {
item.action_params = formatArray(item.action_params); item.action_params = formatArray(item.action_params);
} else if (typeof item.action_params === 'object') { } else if (typeof item.values === 'object') {
item.action_params = generatePayloadForObject(item); item.action_params = [item.action_params.id];
} else if (!item.action_params) { } else if (!item.action_params) {
item.action_params = []; item.action_params = [];
} else { } else {

View file

@ -1,242 +0,0 @@
import {
OPERATOR_TYPES_1,
OPERATOR_TYPES_3,
OPERATOR_TYPES_4,
} from 'dashboard/routes/dashboard/settings/automation/operators';
import filterQueryGenerator from './filterQueryGenerator';
import actionQueryGenerator from './actionQueryGenerator';
const MESSAGE_CONDITION_VALUES = [
{
id: 'incoming',
name: 'Incoming Message',
},
{
id: 'outgoing',
name: 'Outgoing Message',
},
];
export const getCustomAttributeInputType = key => {
const customAttributeMap = {
date: 'date',
text: 'plain_text',
list: 'search_select',
checkbox: 'search_select',
};
return customAttributeMap[key] || 'plain_text';
};
export const isACustomAttribute = (customAttributes, key) => {
return customAttributes.find(attr => {
return attr.attribute_key === key;
});
};
export const getCustomAttributeListDropdownValues = (
customAttributes,
type
) => {
return customAttributes
.find(attr => attr.attribute_key === type)
.attribute_values.map(item => {
return {
id: item,
name: item,
};
});
};
export const isCustomAttributeCheckbox = (customAttributes, key) => {
return customAttributes.find(attr => {
return (
attr.attribute_key === key && attr.attribute_display_type === 'checkbox'
);
});
};
export const isCustomAttributeList = (customAttributes, type) => {
return customAttributes.find(attr => {
return (
attr.attribute_key === type && attr.attribute_display_type === 'list'
);
});
};
export const getOperatorTypes = key => {
const operatorMap = {
list: OPERATOR_TYPES_1,
text: OPERATOR_TYPES_3,
number: OPERATOR_TYPES_1,
link: OPERATOR_TYPES_1,
date: OPERATOR_TYPES_4,
checkbox: OPERATOR_TYPES_1,
};
return operatorMap[key] || OPERATOR_TYPES_1;
};
export const generateCustomAttributeTypes = (customAttributes, type) => {
return customAttributes.map(attr => {
return {
key: attr.attribute_key,
name: attr.attribute_display_name,
inputType: getCustomAttributeInputType(attr.attribute_display_type),
filterOperators: getOperatorTypes(attr.attribute_display_type),
customAttributeType: type,
};
});
};
export const generateConditionOptions = (options, key = 'id') => {
return options.map(i => {
return {
id: i[key],
name: i.title,
};
});
};
export const getActionOptions = ({ teams, labels, type }) => {
const actionsMap = {
assign_team: teams,
send_email_to_team: teams,
add_label: generateConditionOptions(labels, 'title'),
};
return actionsMap[type];
};
export const getConditionOptions = ({
agents,
booleanFilterOptions,
campaigns,
contacts,
countries,
customAttributes,
inboxes,
languages,
statusFilterOptions,
teams,
type,
}) => {
if (isCustomAttributeCheckbox(customAttributes, type)) {
return booleanFilterOptions;
}
if (isCustomAttributeList(customAttributes, type)) {
return getCustomAttributeListDropdownValues(customAttributes, type);
}
const conditionFilterMaps = {
status: statusFilterOptions,
assignee_id: agents,
contact: contacts,
inbox_id: inboxes,
team_id: teams,
campaigns: generateConditionOptions(campaigns),
browser_language: languages,
country_code: countries,
message_type: MESSAGE_CONDITION_VALUES,
};
return conditionFilterMaps[type];
};
export const getFileName = (action, files = []) => {
const blobId = action.action_params[0];
if (!blobId) return '';
if (action.action_name === 'send_attachment') {
const file = files.find(item => item.blob_id === blobId);
if (file) return file.filename.toString();
}
return '';
};
export const getDefaultConditions = eventName => {
if (eventName === 'message_created') {
return [
{
attribute_key: 'message_type',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
custom_attribute_type: '',
},
];
}
return [
{
attribute_key: 'status',
filter_operator: 'equal_to',
values: '',
query_operator: 'and',
custom_attribute_type: '',
},
];
};
export const getDefaultActions = () => {
return [
{
action_name: 'assign_team',
action_params: [],
},
];
};
export const filterCustomAttributes = customAttributes => {
return customAttributes.map(attr => {
return {
key: attr.attribute_key,
name: attr.attribute_display_name,
type: attr.attribute_display_type,
};
});
};
export const getStandardAttributeInputType = (automationTypes, event, key) => {
return automationTypes[event].conditions.find(item => item.key === key)
.inputType;
};
export const generateAutomationPayload = payload => {
const automation = JSON.parse(JSON.stringify(payload));
automation.conditions[automation.conditions.length - 1].query_operator = null;
automation.conditions = filterQueryGenerator(automation.conditions).payload;
automation.actions = actionQueryGenerator(automation.actions);
return automation;
};
export const isCustomAttribute = (attrs, key) => {
return attrs.find(attr => attr.key === key);
};
export const generateCustomAttributes = (
conversationAttributes = [],
contactAttribtues = [],
conversationlabel,
contactlabel
) => {
const customAttributes = [];
if (conversationAttributes.length) {
customAttributes.push(
{
key: `conversation_custom_attribute`,
name: conversationlabel,
disabled: true,
},
...conversationAttributes
);
}
if (contactAttribtues.length) {
customAttributes.push(
{
key: `contact_custom_attribute`,
name: contactlabel,
disabled: true,
},
...contactAttribtues
);
}
return customAttributes;
};

View file

@ -1,3 +1,15 @@
const lowerCaseValues = (operator, values) => {
if (operator === 'equal_to' || operator === 'not_equal_to') {
values = values.map(val => {
if (typeof val === 'string') {
return val.toLowerCase();
}
return val;
});
}
return values;
};
const generatePayload = data => { const generatePayload = data => {
// Make a copy of data to avoid vue data reactivity issues // Make a copy of data to avoid vue data reactivity issues
const filters = JSON.parse(JSON.stringify(data)); const filters = JSON.parse(JSON.stringify(data));
@ -11,6 +23,8 @@ const generatePayload = data => {
} else { } else {
item.values = [item.values]; item.values = [item.values];
} }
// Convert all values to lowerCase if operator_type is 'equal_to' or 'not_equal_to'
item.values = lowerCaseValues(item.filter_operator, item.values);
return item; return item;
}); });
// For every query added, the query_operator is set default to and so the // For every query added, the query_operator is set default to and so the

View file

@ -60,11 +60,15 @@ export const getFormattedPreChatFields = ({ preChatFields }) => {
return { return {
...item, ...item,
label: getLabel({ label: getLabel({
key: item.name, key: standardFieldKeys[item.name]
? standardFieldKeys[item.name].key
: item.name,
label: item.label ? item.label : item.name, label: item.label ? item.label : item.name,
}), }),
placeholder: getPlaceHolder({ placeholder: getPlaceHolder({
key: item.name, key: standardFieldKeys[item.name]
? standardFieldKeys[item.name].key
: item.name,
placeholder: item.placeholder ? item.placeholder : item.name, placeholder: item.placeholder ? item.placeholder : item.name,
}), }),
}; };

View file

@ -1,4 +1,4 @@
import AnalyticsHelper from './AnalyticsHelper'; import posthog from 'posthog-js';
export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER'; export const CHATWOOT_SET_USER = 'CHATWOOT_SET_USER';
export const CHATWOOT_RESET = 'CHATWOOT_RESET'; export const CHATWOOT_RESET = 'CHATWOOT_RESET';
@ -8,9 +8,16 @@ export const ANALYTICS_RESET = 'ANALYTICS_RESET';
export const initializeAnalyticsEvents = () => { export const initializeAnalyticsEvents = () => {
window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => { window.bus.$on(ANALYTICS_IDENTITY, ({ user }) => {
AnalyticsHelper.identify(user); if (window.analyticsConfig) {
posthog.identify(user.id, { name: user.name, email: user.email });
}
});
window.bus.$on(ANALYTICS_RESET, () => {
if (window.analyticsConfig) {
posthog.reset();
}
}); });
window.bus.$on(ANALYTICS_RESET, () => {});
}; };
export const initializeChatwootEvents = () => { export const initializeChatwootEvents = () => {

View file

@ -29,12 +29,6 @@ describe('#URL Helpers', () => {
'/app/accounts/1/team/1' '/app/accounts/1/team/1'
); );
}); });
it('should return url to custom view', () => {
expect(conversationListPageURL({ accountId: 1, customViewId: 1 })).toBe(
'/app/accounts/1/custom_view/1'
);
});
}); });
describe('conversationUrl', () => { describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => { it('should return direct conversation URL if activeInbox is nil', () => {

View file

@ -5,7 +5,7 @@ const testData = [
attribute_key: 'status', attribute_key: 'status',
filter_operator: 'equal_to', filter_operator: 'equal_to',
values: [ values: [
{ id: 'pending', name: 'Pending' }, { id: 'PENDING', name: 'Pending' },
{ id: 'resolved', name: 'Resolved' }, { id: 'resolved', name: 'Resolved' },
], ],
query_operator: 'and', query_operator: 'and',
@ -18,7 +18,7 @@ const testData = [
account_id: 1, account_id: 1,
auto_offline: true, auto_offline: true,
confirmed: true, confirmed: true,
email: 'fayaz@test.com', email: 'fayazara@gmail.com',
available_name: 'Fayaz', available_name: 'Fayaz',
name: 'Fayaz', name: 'Fayaz',
role: 'agent', role: 'agent',
@ -52,7 +52,7 @@ const finalResult = {
{ {
attribute_key: 'id', attribute_key: 'id',
filter_operator: 'equal_to', filter_operator: 'equal_to',
values: ['This is a test'], values: ['this is a test'],
}, },
], ],
}; };

View file

@ -15,7 +15,6 @@ import id from './locale/id';
import it from './locale/it'; import it from './locale/it';
import ja from './locale/ja'; import ja from './locale/ja';
import ko from './locale/ko'; import ko from './locale/ko';
import lv from './locale/lv';
import ml from './locale/ml'; import ml from './locale/ml';
import nl from './locale/nl'; import nl from './locale/nl';
import no from './locale/no'; import no from './locale/no';
@ -53,7 +52,6 @@ export default {
ja, ja,
ko, ko,
ml, ml,
lv,
nl, nl,
no, no,
pl, pl,

View file

@ -1,70 +1,5 @@
{ {
"AGENT_BOTS": { "AGENT_BOTS": {
"HEADER": "Bots", "HEADER": "Bots"
"LOADING_EDITOR": "Loading Editor...",
"HEADER_BTN_TXT": "Add Bot Configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot Name",
"PLACEHOLDER": "Give your bot a name",
"ERROR": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Bot Description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above",
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again."
},
"SUBMIT": "Validate and save"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.",
"SUBMIT": "تحديث",
"SUCCESS_MESSAGE": "Successfully updated the agent bot",
"ERROR_MESSAGE": "Could not update the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Select Bot"
},
"ADD": {
"TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully",
"ERROR_MESSAGE": "Could not add bot, Please try again later"
}
},
"LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
"LOADING": "Fetching Bots...",
"TYPE": "Bot Type"
},
"DELETE": {
"BUTTON_TEXT": "حذف",
"TITLE": "Delete Bot",
"SUBMIT": "حذف",
"CANCEL_BUTTON_TEXT": "إلغاء",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
}
},
"EDIT": {
"BUTTON_TEXT": "تعديل",
"LOADING": "Fetching Bots...",
"TITLE": "Edit Bot",
"CANCEL_BUTTON_TEXT": "إلغاء",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot, Please try again later"
}
},
"TYPES": {
"WEBHOOK": "Webhook Bot",
"CSML": "CSML Bot"
}
} }
} }

View file

@ -86,9 +86,7 @@
"RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه" "RESET_MESSAGE": "تغيير نوع الحدث سوف يعيد تعيين الشروط والأحداث التي أضفتها أدناه"
}, },
"CONDITION": { "CONDITION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ", "DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ"
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
}, },
"ACTION": { "ACTION": {
"DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ", "DELETE_MESSAGE": "يجب أن يكون لديك على الأقل شرط واحد للحفظ",

View file

@ -8,7 +8,6 @@
"ASSIGN_LABEL": "تكليف", "ASSIGN_LABEL": "تكليف",
"YES": "نعم", "YES": "نعم",
"ASSIGN_AGENT_TOOLTIP": "إسناد وكيل", "ASSIGN_AGENT_TOOLTIP": "إسناد وكيل",
"ASSIGN_TEAM_TOOLTIP": "تعيين فريق",
"ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح", "ASSIGN_SUCCESFUL": "تم تعيين المحادثات بنجاح",
"ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى", "ASSIGN_FAILED": "فشل في تعيين المحادثات، الرجاء المحاولة مرة أخرى",
"RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح", "RESOLVE_SUCCESFUL": "تم تسوية المحادثات بنجاح",
@ -27,14 +26,6 @@
"ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة", "ASSIGN_SELECTED_LABELS": "تعيين التسميات المحددة",
"ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح", "ASSIGN_SUCCESFUL": "تم تعيين التسميات بنجاح",
"ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى" "ASSIGN_FAILED": "فشل في تعيين التسميات ، الرجاء المحاولة مرة أخرى"
},
"TEAMS": {
"TEAM_SELECT_LABEL": "اختيار فريق",
"NONE": "لا شيء",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team",
"ASSIGN_SUCCESFUL": "Teams assiged successfully",
"ASSIGN_FAILED": "Failed to assign team, please try again"
} }
} }
} }

View file

@ -8,7 +8,6 @@
}, },
"TAB_HEADING": "المحادثات", "TAB_HEADING": "المحادثات",
"MENTION_HEADING": "الإشارات", "MENTION_HEADING": "الإشارات",
"UNATTENDED_HEADING": "بدون حضور",
"SEARCH": { "SEARCH": {
"INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .." "INPUT": "البحث عن جهات الاتصال، المحادثات، قوالب الردود الجاهزة .."
}, },
@ -57,8 +56,6 @@
"REPLY_TO_TWEET": "الرد على هذه التغريدة", "REPLY_TO_TWEET": "الرد على هذه التغريدة",
"LINK_TO_STORY": "الذهاب إلى قصة الإنستقرام", "LINK_TO_STORY": "الذهاب إلى قصة الإنستقرام",
"SENT": "Sent successfully", "SENT": "Sent successfully",
"READ": "Read successfully",
"DELIVERED": "Delivered successfully",
"NO_MESSAGES": "لا توجد رسائل", "NO_MESSAGES": "لا توجد رسائل",
"NO_CONTENT": "لم يتم العثور على محتوى", "NO_CONTENT": "لم يتم العثور على محتوى",
"HIDE_QUOTED_TEXT": "Hide Quoted Text", "HIDE_QUOTED_TEXT": "Hide Quoted Text",

View file

@ -41,10 +41,6 @@
"NO_RESPONSE": "لا توجد استجابة", "NO_RESPONSE": "لا توجد استجابة",
"RATING_TITLE": "التقييم", "RATING_TITLE": "التقييم",
"FEEDBACK_TITLE": "الملاحظات", "FEEDBACK_TITLE": "الملاحظات",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
},
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "إغلاق المحادثة", "RESOLVE_ACTION": "إغلاق المحادثة",
"REOPEN_ACTION": "إعادة فتح", "REOPEN_ACTION": "إعادة فتح",
@ -68,7 +64,6 @@
"CARD_CONTEXT_MENU": { "CARD_CONTEXT_MENU": {
"PENDING": "تحديد كمعلق", "PENDING": "تحديد كمعلق",
"RESOLVED": "تحديد كمحلولة", "RESOLVED": "تحديد كمحلولة",
"MARK_AS_UNREAD": "Mark as unread",
"REOPEN": "إعادة فتح المحادثة", "REOPEN": "إعادة فتح المحادثة",
"SNOOZE": { "SNOOZE": {
"TITLE": "غفوة", "TITLE": "غفوة",

View file

@ -1,6 +0,0 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
}
}

View file

@ -23,7 +23,7 @@
"ERROR": "الرجاء إدخال اسم حساب صحيح" "ERROR": "الرجاء إدخال اسم حساب صحيح"
}, },
"LANGUAGE": { "LANGUAGE": {
"LABEL": "Site language", "LABEL": "لغة الموقع (تجريبي)",
"PLACEHOLDER": "اسم الحساب الخاص بك", "PLACEHOLDER": "اسم الحساب الخاص بك",
"ERROR": "" "ERROR": ""
}, },
@ -54,8 +54,7 @@
"MULTISELECT": { "MULTISELECT": {
"ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار", "ENTER_TO_SELECT": "اضغط على زر الإدخال للاختيار",
"ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف", "ENTER_TO_REMOVE": "اضغط على زر الإدخال للحذف",
"SELECT_ONE": "اختر واحدا", "SELECT_ONE": "اختر واحدا"
"SELECT": "Select"
} }
}, },
"NOTIFICATIONS_PAGE": { "NOTIFICATIONS_PAGE": {
@ -137,8 +136,5 @@
"UNTIL_NEXT_WEEK": "حتى الأسبوع القادم", "UNTIL_NEXT_WEEK": "حتى الأسبوع القادم",
"UNTIL_TOMORROW": "حتى الغد" "UNTIL_TOMORROW": "حتى الغد"
} }
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
} }
} }

View file

@ -134,7 +134,7 @@
"PHONE_NUMBER": { "PHONE_NUMBER": {
"LABEL": "رقم الهاتف", "LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.", "PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces." "ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
}, },
"API_CALLBACK": { "API_CALLBACK": {
"TITLE": "عنوان Callback URL", "TITLE": "عنوان Callback URL",
@ -185,7 +185,7 @@
"PHONE_NUMBER": { "PHONE_NUMBER": {
"LABEL": "رقم الهاتف", "LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.", "PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces." "ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
}, },
"SUBMIT_BUTTON": "إنشاء قناة عرض التردد", "SUBMIT_BUTTON": "إنشاء قناة عرض التردد",
"API": { "API": {
@ -214,7 +214,7 @@
"PHONE_NUMBER": { "PHONE_NUMBER": {
"LABEL": "رقم الهاتف", "LABEL": "رقم الهاتف",
"PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.", "PLACEHOLDER": "الرجاء إدخال رقم الهاتف الذي سيتم إرسال الرسائل منه.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces." "ERROR": "الرجاء إدخال قيمة صحيحة. يجب أن يبدأ رقم الهاتف بعلامة `+`."
}, },
"PHONE_NUMBER_ID": { "PHONE_NUMBER_ID": {
"LABEL": "رقم الهاتف", "LABEL": "رقم الهاتف",
@ -388,10 +388,6 @@
"ENABLED": "مفعل", "ENABLED": "مفعل",
"DISABLED": "معطّل" "DISABLED": "معطّل"
}, },
"LOCK_TO_SINGLE_CONVERSATION": {
"ENABLED": "مفعل",
"DISABLED": "معطّل"
},
"ENABLE_HMAC": { "ENABLE_HMAC": {
"LABEL": "تمكين" "LABEL": "تمكين"
} }
@ -420,8 +416,7 @@
"CAMPAIGN": "الحملات", "CAMPAIGN": "الحملات",
"PRE_CHAT_FORM": "نموذج ما قبل الدردشة", "PRE_CHAT_FORM": "نموذج ما قبل الدردشة",
"BUSINESS_HOURS": "ساعات العمل", "BUSINESS_HOURS": "ساعات العمل",
"WIDGET_BUILDER": "منشئ اللايف شات", "WIDGET_BUILDER": "منشئ اللايف شات"
"BOT_CONFIGURATION": "Bot Configuration"
}, },
"SETTINGS": "الإعدادات", "SETTINGS": "الإعدادات",
"FEATURES": { "FEATURES": {
@ -445,8 +440,6 @@
"ENABLE_CSAT_SUB_TEXT": "تمكين/تعطيل تقييم خدمة العملاء بعد إنتهاء المحادثة", "ENABLE_CSAT_SUB_TEXT": "تمكين/تعطيل تقييم خدمة العملاء بعد إنتهاء المحادثة",
"ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني", "ENABLE_CONTINUITY_VIA_EMAIL": "تمكين استمرارية المحادثة عبر البريد الإلكتروني",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.", "ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "المحادثات ستستمر عبر البريد الإلكتروني إذا كان عنوان البريد الإلكتروني لجهة الاتصال متاحاً.",
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
"INBOX_UPDATE_TITLE": "إعدادات قناة التواصل", "INBOX_UPDATE_TITLE": "إعدادات قناة التواصل",
"INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل", "INBOX_UPDATE_SUB_TEXT": "تحديث إعدادات قناة التواصل",
"AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.", "AUTO_ASSIGNMENT_SUB_TEXT": "تمكين أو تعطيل الإسناد التلقائي للمحادثات الجديدة إلى الموظفين المضافين إلى قناة التواصل هذه.",

View file

@ -103,9 +103,7 @@
"متصل", "متصل",
"مشغول", "مشغول",
"غير متصل" "غير متصل"
], ]
"SET_AVAILABILITY_SUCCESS": "Availability has been set successfully",
"SET_AVAILABILITY_ERROR": "Couldn't set availability, please try again"
}, },
"EMAIL": { "EMAIL": {
"LABEL": "عنوان البريد الإلكتروني الخاص بك", "LABEL": "عنوان البريد الإلكتروني الخاص بك",
@ -136,7 +134,6 @@
"SELECTOR_SUBTITLE": "اختر حساباً من القائمة التالية", "SELECTOR_SUBTITLE": "اختر حساباً من القائمة التالية",
"PROFILE_SETTINGS": "إعدادات الملف الشخصي", "PROFILE_SETTINGS": "إعدادات الملف الشخصي",
"KEYBOARD_SHORTCUTS": "اختصارات لوحة المفاتيح", "KEYBOARD_SHORTCUTS": "اختصارات لوحة المفاتيح",
"SUPER_ADMIN_CONSOLE": "Super Admin Console",
"LOGOUT": "تسجيل الخروج" "LOGOUT": "تسجيل الخروج"
}, },
"APP_GLOBAL": { "APP_GLOBAL": {
@ -161,9 +158,6 @@
"DOWNLOAD": "تنزيل", "DOWNLOAD": "تنزيل",
"UPLOADING": "جاري الرفع..." "UPLOADING": "جاري الرفع..."
}, },
"LOCATION_BUBBLE": {
"SEE_ON_MAP": "See on map"
},
"FORM_BUBBLE": { "FORM_BUBBLE": {
"SUBMIT": "إرسال" "SUBMIT": "إرسال"
} }
@ -180,7 +174,6 @@
"CONVERSATIONS": "المحادثات", "CONVERSATIONS": "المحادثات",
"ALL_CONVERSATIONS": "كل المحادثات", "ALL_CONVERSATIONS": "كل المحادثات",
"MENTIONED_CONVERSATIONS": "الإشارات", "MENTIONED_CONVERSATIONS": "الإشارات",
"UNATTENDED_CONVERSATIONS": "بدون حضور",
"REPORTS": "التقارير", "REPORTS": "التقارير",
"SETTINGS": "الإعدادات", "SETTINGS": "الإعدادات",
"CONTACTS": "جهات الاتصال", "CONTACTS": "جهات الاتصال",
@ -229,10 +222,6 @@
"CATEGORY": "الفئة", "CATEGORY": "الفئة",
"CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات" "CATEGORY_EMPTY_MESSAGE": "لم يتم العثور على فئات"
}, },
"SET_AUTO_OFFLINE": {
"TEXT": "Mark offline automatically",
"INFO_TEXT": "Let the system automatically mark you offline when you aren't using the app or dashboard."
},
"DOCS": "قراءة المستندات" "DOCS": "قراءة المستندات"
}, },
"BILLING_SETTINGS": { "BILLING_SETTINGS": {
@ -264,7 +253,7 @@
}, },
"FORM": { "FORM": {
"NAME": { "NAME": {
"LABEL": "اسم الشركة", "LABEL": "اسم الحساب",
"PLACEHOLDER": "مؤسسة Wayne" "PLACEHOLDER": "مؤسسة Wayne"
}, },
"SUBMIT": "إرسال" "SUBMIT": "إرسال"

View file

@ -2,13 +2,11 @@
"REGISTER": { "REGISTER": {
"TRY_WOOT": "تسجيل حساب", "TRY_WOOT": "تسجيل حساب",
"TITLE": "تسجيل", "TITLE": "تسجيل",
"TESTIMONIAL_HEADER": "All it takes is one step to move forward",
"TESTIMONIAL_CONTENT": "You're one step away from engaging your customers, retaining them and finding new ones.",
"TERMS_ACCEPT": "من خلال التسجيل، فإنك توافق على <a href=\"https://www.chatwoot.com/terms\">شروط الخدمة</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">سياسة الخصوصية</a>", "TERMS_ACCEPT": "من خلال التسجيل، فإنك توافق على <a href=\"https://www.chatwoot.com/terms\">شروط الخدمة</a> و <a href=\"https://www.chatwoot.com/privacy-policy\">سياسة الخصوصية</a>",
"COMPANY_NAME": { "ACCOUNT_NAME": {
"LABEL": "Company name", "LABEL": "اسم الحساب",
"PLACEHOLDER": "Enter your company name. eg: Wayne Enterprises", "PLACEHOLDER": "أدخل اسم الحساب. مثال: Wayne Enterprises",
"ERROR": "Company name is too short" "ERROR": "اسم الحساب قصير جداً"
}, },
"FULL_NAME": { "FULL_NAME": {
"LABEL": "الاسم الكامل", "LABEL": "الاسم الكامل",
@ -18,7 +16,7 @@
"EMAIL": { "EMAIL": {
"LABEL": "البريد الإلكتروني للعمل", "LABEL": "البريد الإلكتروني للعمل",
"PLACEHOLDER": "أدخل عنوان بريدك الإلكتروني للعمل. مثال: bruce@wayne.enterprises", "PLACEHOLDER": "أدخل عنوان بريدك الإلكتروني للعمل. مثال: bruce@wayne.enterprises",
"ERROR": "Please enter a valid work email address" "ERROR": "عنوان البريد الإلكتروني غير صالح"
}, },
"PASSWORD": { "PASSWORD": {
"LABEL": "كلمة المرور", "LABEL": "كلمة المرور",

View file

@ -1,70 +1,5 @@
{ {
"AGENT_BOTS": { "AGENT_BOTS": {
"HEADER": "Bots", "HEADER": "Bots"
"LOADING_EDITOR": "Loading Editor...",
"HEADER_BTN_TXT": "Add Bot Configuration",
"SIDEBAR_TXT": "<p><b>Agent Bots</b> <p>Agent bots allows you to automate the conversations</p>",
"CSML_BOT_EDITOR": {
"NAME": {
"LABEL": "Bot Name",
"PLACEHOLDER": "Give your bot a name",
"ERROR": "Bot name is required"
},
"DESCRIPTION": {
"LABEL": "Bot Description",
"PLACEHOLDER": "What does this bot do?"
},
"BOT_CONFIG": {
"ERROR": "Please enter your CSML bot configuration above",
"API_ERROR": "Your CSML configuration is invalid, please fix it and try again."
},
"SUBMIT": "Validate and save"
},
"BOT_CONFIGURATION": {
"TITLE": "Select an agent bot",
"DESC": "You can set an agent bot from the list to this inbox. The bot can initially handle the conversation and transfer it to an agent when needed.",
"SUBMIT": "Обновяване",
"SUCCESS_MESSAGE": "Successfully updated the agent bot",
"ERROR_MESSAGE": "Could not update the agent bot, please try again later",
"SELECT_PLACEHOLDER": "Select Bot"
},
"ADD": {
"TITLE": "Configure new bot",
"CANCEL_BUTTON_TEXT": "Отмени",
"API": {
"SUCCESS_MESSAGE": "Bot added successfully",
"ERROR_MESSAGE": "Could not add bot, Please try again later"
}
},
"LIST": {
"404": "No Bots found, you can create a bot by clicking the 'Configure new bot' Button ↗",
"LOADING": "Fetching Bots...",
"TYPE": "Bot Type"
},
"DELETE": {
"BUTTON_TEXT": "Изтрий",
"TITLE": "Delete Bot",
"SUBMIT": "Изтрий",
"CANCEL_BUTTON_TEXT": "Отмени",
"DESCRIPTION": "Are you sure you want to delete this bot? This action is irreversible",
"API": {
"SUCCESS_MESSAGE": "Bot deleted successfully",
"ERROR_MESSAGE": "Could not able to delete bot, Please try again later"
}
},
"EDIT": {
"BUTTON_TEXT": "Редактирай",
"LOADING": "Fetching Bots...",
"TITLE": "Edit Bot",
"CANCEL_BUTTON_TEXT": "Отмени",
"API": {
"SUCCESS_MESSAGE": "Bot updated successfully",
"ERROR_MESSAGE": "Could not update bot, Please try again later"
}
},
"TYPES": {
"WEBHOOK": "Webhook Bot",
"CSML": "CSML Bot"
}
} }
} }

View file

@ -86,9 +86,7 @@
"RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below" "RESET_MESSAGE": "Changing event type will reset the conditions and events you have added below"
}, },
"CONDITION": { "CONDITION": {
"DELETE_MESSAGE": "You need to have atleast one condition to save", "DELETE_MESSAGE": "You need to have atleast one condition to save"
"CONTACT_CUSTOM_ATTR_LABEL": "Contact Custom Attributes",
"CONVERSATION_CUSTOM_ATTR_LABEL": "Conversation Custom Attributes"
}, },
"ACTION": { "ACTION": {
"DELETE_MESSAGE": "You need to have atleast one action to save", "DELETE_MESSAGE": "You need to have atleast one action to save",

View file

@ -8,7 +8,6 @@
"ASSIGN_LABEL": "Assign", "ASSIGN_LABEL": "Assign",
"YES": "Yes", "YES": "Yes",
"ASSIGN_AGENT_TOOLTIP": "Assign Agent", "ASSIGN_AGENT_TOOLTIP": "Assign Agent",
"ASSIGN_TEAM_TOOLTIP": "Assign team",
"ASSIGN_SUCCESFUL": "Conversations assigned successfully", "ASSIGN_SUCCESFUL": "Conversations assigned successfully",
"ASSIGN_FAILED": "Failed to assign conversations, please try again", "ASSIGN_FAILED": "Failed to assign conversations, please try again",
"RESOLVE_SUCCESFUL": "Conversations resolved successfully", "RESOLVE_SUCCESFUL": "Conversations resolved successfully",
@ -27,14 +26,6 @@
"ASSIGN_SELECTED_LABELS": "Assign selected labels", "ASSIGN_SELECTED_LABELS": "Assign selected labels",
"ASSIGN_SUCCESFUL": "Labels assigned successfully", "ASSIGN_SUCCESFUL": "Labels assigned successfully",
"ASSIGN_FAILED": "Failed to assign labels, please try again" "ASSIGN_FAILED": "Failed to assign labels, please try again"
},
"TEAMS": {
"TEAM_SELECT_LABEL": "Select Team",
"NONE": "Нито един",
"NO_TEAMS_AVAILABLE": "There are no teams added to this account yet.",
"ASSIGN_SELECTED_TEAMS": "Assign selected team",
"ASSIGN_SUCCESFUL": "Teams assiged successfully",
"ASSIGN_FAILED": "Failed to assign team, please try again"
} }
} }
} }

View file

@ -8,7 +8,6 @@
}, },
"TAB_HEADING": "Разговори", "TAB_HEADING": "Разговори",
"MENTION_HEADING": "Споменавания", "MENTION_HEADING": "Споменавания",
"UNATTENDED_HEADING": "Unattended",
"SEARCH": { "SEARCH": {
"INPUT": "Търсене на хора, чатове, запазени отговори .." "INPUT": "Търсене на хора, чатове, запазени отговори .."
}, },
@ -57,8 +56,6 @@
"REPLY_TO_TWEET": "Отговори на този туит", "REPLY_TO_TWEET": "Отговори на този туит",
"LINK_TO_STORY": "Go to instagram story", "LINK_TO_STORY": "Go to instagram story",
"SENT": "Успено изпратено", "SENT": "Успено изпратено",
"READ": "Read successfully",
"DELIVERED": "Delivered successfully",
"NO_MESSAGES": "Няма съобщения", "NO_MESSAGES": "Няма съобщения",
"NO_CONTENT": "Няма налично съдържание", "NO_CONTENT": "Няма налично съдържание",
"HIDE_QUOTED_TEXT": "Скриване на цитирания текст", "HIDE_QUOTED_TEXT": "Скриване на цитирания текст",

View file

@ -41,10 +41,6 @@
"NO_RESPONSE": "No response", "NO_RESPONSE": "No response",
"RATING_TITLE": "Rating", "RATING_TITLE": "Rating",
"FEEDBACK_TITLE": "Feedback", "FEEDBACK_TITLE": "Feedback",
"CARD": {
"SHOW_LABELS": "Show labels",
"HIDE_LABELS": "Hide labels"
},
"HEADER": { "HEADER": {
"RESOLVE_ACTION": "Resolve", "RESOLVE_ACTION": "Resolve",
"REOPEN_ACTION": "Reopen", "REOPEN_ACTION": "Reopen",
@ -68,7 +64,6 @@
"CARD_CONTEXT_MENU": { "CARD_CONTEXT_MENU": {
"PENDING": "Mark as pending", "PENDING": "Mark as pending",
"RESOLVED": "Mark as resolved", "RESOLVED": "Mark as resolved",
"MARK_AS_UNREAD": "Mark as unread",
"REOPEN": "Reopen conversation", "REOPEN": "Reopen conversation",
"SNOOZE": { "SNOOZE": {
"TITLE": "Snooze", "TITLE": "Snooze",

View file

@ -1,6 +0,0 @@
{
"EMOJI": {
"PLACEHOLDER": "Search emojis",
"NOT_FOUND": "No emoji match your search"
}
}

View file

@ -23,7 +23,7 @@
"ERROR": "Please enter a valid account name" "ERROR": "Please enter a valid account name"
}, },
"LANGUAGE": { "LANGUAGE": {
"LABEL": "Site language", "LABEL": "Site language (Beta)",
"PLACEHOLDER": "Your account name", "PLACEHOLDER": "Your account name",
"ERROR": "" "ERROR": ""
}, },
@ -54,8 +54,7 @@
"MULTISELECT": { "MULTISELECT": {
"ENTER_TO_SELECT": "Press enter to select", "ENTER_TO_SELECT": "Press enter to select",
"ENTER_TO_REMOVE": "Press enter to remove", "ENTER_TO_REMOVE": "Press enter to remove",
"SELECT_ONE": "Select one", "SELECT_ONE": "Select one"
"SELECT": "Select"
} }
}, },
"NOTIFICATIONS_PAGE": { "NOTIFICATIONS_PAGE": {
@ -137,8 +136,5 @@
"UNTIL_NEXT_WEEK": "Until next week", "UNTIL_NEXT_WEEK": "Until next week",
"UNTIL_TOMORROW": "Until tomorrow" "UNTIL_TOMORROW": "Until tomorrow"
} }
},
"DASHBOARD_APPS": {
"LOADING_MESSAGE": "Loading Dashboard App..."
} }
} }

View file

@ -134,7 +134,7 @@
"PHONE_NUMBER": { "PHONE_NUMBER": {
"LABEL": "Phone number", "LABEL": "Phone number",
"PLACEHOLDER": "Please enter the phone number from which message will be sent.", "PLACEHOLDER": "Please enter the phone number from which message will be sent.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces." "ERROR": "Please enter a valid value. Phone number should start with `+` sign."
}, },
"API_CALLBACK": { "API_CALLBACK": {
"TITLE": "Callback URL", "TITLE": "Callback URL",
@ -185,7 +185,7 @@
"PHONE_NUMBER": { "PHONE_NUMBER": {
"LABEL": "Телефон", "LABEL": "Телефон",
"PLACEHOLDER": "Please enter the phone number from which message will be sent.", "PLACEHOLDER": "Please enter the phone number from which message will be sent.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces." "ERROR": "Please enter a valid value. Phone number should start with `+` sign."
}, },
"SUBMIT_BUTTON": "Create Bandwidth Channel", "SUBMIT_BUTTON": "Create Bandwidth Channel",
"API": { "API": {
@ -214,7 +214,7 @@
"PHONE_NUMBER": { "PHONE_NUMBER": {
"LABEL": "Phone number", "LABEL": "Phone number",
"PLACEHOLDER": "Please enter the phone number from which message will be sent.", "PLACEHOLDER": "Please enter the phone number from which message will be sent.",
"ERROR": "Please provide a valid phone number that starts with a `+` sign and does not contain any spaces." "ERROR": "Please enter a valid value. Phone number should start with `+` sign."
}, },
"PHONE_NUMBER_ID": { "PHONE_NUMBER_ID": {
"LABEL": "Phone number ID", "LABEL": "Phone number ID",
@ -388,10 +388,6 @@
"ENABLED": "Включен", "ENABLED": "Включен",
"DISABLED": "Изключен" "DISABLED": "Изключен"
}, },
"LOCK_TO_SINGLE_CONVERSATION": {
"ENABLED": "Включен",
"DISABLED": "Изключен"
},
"ENABLE_HMAC": { "ENABLE_HMAC": {
"LABEL": "Enable" "LABEL": "Enable"
} }
@ -420,8 +416,7 @@
"CAMPAIGN": "Campaigns", "CAMPAIGN": "Campaigns",
"PRE_CHAT_FORM": "Pre Chat Form", "PRE_CHAT_FORM": "Pre Chat Form",
"BUSINESS_HOURS": "Business Hours", "BUSINESS_HOURS": "Business Hours",
"WIDGET_BUILDER": "Widget Builder", "WIDGET_BUILDER": "Widget Builder"
"BOT_CONFIGURATION": "Bot Configuration"
}, },
"SETTINGS": "Settings", "SETTINGS": "Settings",
"FEATURES": { "FEATURES": {
@ -445,8 +440,6 @@
"ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation", "ENABLE_CSAT_SUB_TEXT": "Enable/Disable CSAT(Customer satisfaction) survey after resolving a conversation",
"ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email", "ENABLE_CONTINUITY_VIA_EMAIL": "Enable conversation continuity via email",
"ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.", "ENABLE_CONTINUITY_VIA_EMAIL_SUB_TEXT": "Conversations will continue over email if the contact email address is available.",
"LOCK_TO_SINGLE_CONVERSATION": "Lock to single conversation",
"LOCK_TO_SINGLE_CONVERSATION_SUB_TEXT": "Enable or disable multiple conversations for the same contact in this inbox",
"INBOX_UPDATE_TITLE": "Inbox Settings", "INBOX_UPDATE_TITLE": "Inbox Settings",
"INBOX_UPDATE_SUB_TEXT": "Update your inbox settings", "INBOX_UPDATE_SUB_TEXT": "Update your inbox settings",
"AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.", "AUTO_ASSIGNMENT_SUB_TEXT": "Enable or disable the automatic assignment of new conversations to the agents added to this inbox.",

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