diff --git a/Gemfile.lock b/Gemfile.lock index 857319fc4..74a59167a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -561,7 +561,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.12) + rack (2.2.13) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.5.0) diff --git a/app/channels/room_channel.rb b/app/channels/room_channel.rb index 0680f1458..629078229 100644 --- a/app/channels/room_channel.rb +++ b/app/channels/room_channel.rb @@ -2,10 +2,9 @@ class RoomChannel < ApplicationCable::Channel def subscribed # TODO: should we only do ensure stream if current account is present? # for now going ahead with guard clauses in update_subscription and broadcast_presence - - ensure_stream current_user current_account + ensure_stream update_subscription broadcast_presence end @@ -22,12 +21,12 @@ class RoomChannel < ApplicationCable::Channel data = { account_id: @current_account.id, users: ::OnlineStatusTracker.get_available_users(@current_account.id) } data[:contacts] = ::OnlineStatusTracker.get_available_contacts(@current_account.id) if @current_user.is_a? User - ActionCable.server.broadcast(@pubsub_token, { event: 'presence.update', data: data }) + ActionCable.server.broadcast(pubsub_token, { event: 'presence.update', data: data }) end def ensure_stream - @pubsub_token = params[:pubsub_token] - stream_from @pubsub_token + stream_from pubsub_token + stream_from "account_#{@current_account.id}" if @current_account.present? && @current_user.is_a?(User) end def update_subscription @@ -36,11 +35,15 @@ class RoomChannel < ApplicationCable::Channel ::OnlineStatusTracker.update_presence(@current_account.id, @current_user.class.name, @current_user.id) end + def pubsub_token + @pubsub_token ||= params[:pubsub_token] + end + def current_user @current_user ||= if params[:user_id].blank? - ContactInbox.find_by!(pubsub_token: @pubsub_token).contact + ContactInbox.find_by!(pubsub_token: pubsub_token).contact else - User.find_by!(pubsub_token: @pubsub_token, id: params[:user_id]) + User.find_by!(pubsub_token: pubsub_token, id: params[:user_id]) end end diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 2cd5281ff..138c2bd68 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -6,6 +6,8 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro before_action :conversation, except: [:index, :meta, :search, :create, :filter] before_action :inbox, :contact, :contact_inbox, only: [:create] + ATTACHMENT_RESULTS_PER_PAGE = 100 + def index result = conversation_finder.perform @conversations = result[:conversations] @@ -24,7 +26,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def attachments + @attachments_count = @conversation.attachments.count @attachments = @conversation.attachments + .includes(:message) + .order(created_at: :desc) + .page(attachment_params[:page]) + .per(ATTACHMENT_RESULTS_PER_PAGE) end def show; end @@ -124,6 +131,10 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro params.permit(:priority) end + def attachment_params + params.permit(:page) + end + 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) diff --git a/app/controllers/api/v1/widget/campaigns_controller.rb b/app/controllers/api/v1/widget/campaigns_controller.rb index cb9b96a38..10a73aa62 100644 --- a/app/controllers/api/v1/widget/campaigns_controller.rb +++ b/app/controllers/api/v1/widget/campaigns_controller.rb @@ -2,6 +2,10 @@ class Api::V1::Widget::CampaignsController < Api::V1::Widget::BaseController skip_before_action :set_contact def index - @campaigns = @web_widget.inbox.campaigns.where(enabled: true) + @campaigns = @web_widget + .inbox + .campaigns + .where(enabled: true, account_id: @web_widget.inbox.account_id) + .includes(:sender) end end diff --git a/app/controllers/api/v1/widget/inbox_members_controller.rb b/app/controllers/api/v1/widget/inbox_members_controller.rb index c4bc377ea..22934388b 100644 --- a/app/controllers/api/v1/widget/inbox_members_controller.rb +++ b/app/controllers/api/v1/widget/inbox_members_controller.rb @@ -2,6 +2,6 @@ class Api::V1::Widget::InboxMembersController < Api::V1::Widget::BaseController skip_before_action :set_contact def index - @inbox_members = @web_widget.inbox.inbox_members.includes(:user) + @inbox_members = @web_widget.inbox.inbox_members.includes(user: { avatar_attachment: :blob }) end end diff --git a/app/controllers/concerns/switch_locale.rb b/app/controllers/concerns/switch_locale.rb index 3013ff3cc..a8ea8ae05 100644 --- a/app/controllers/concerns/switch_locale.rb +++ b/app/controllers/concerns/switch_locale.rb @@ -5,10 +5,11 @@ module SwitchLocale def switch_locale(&) # priority is for locale set in query string (mostly for widget/from js sdk) - locale ||= locale_from_params + locale ||= params[:locale] + locale ||= locale_from_custom_domain # if locale is not set in account, let's use DEFAULT_LOCALE env variable - locale ||= locale_from_env_variable + locale ||= ENV.fetch('DEFAULT_LOCALE', nil) set_locale(locale, &) end @@ -32,26 +33,30 @@ module SwitchLocale end def set_locale(locale, &) - # if locale is empty, use default_locale - locale ||= I18n.default_locale + safe_locale = validate_and_get_locale(locale) # Ensure locale won't bleed into other requests # https://guides.rubyonrails.org/i18n.html#managing-the-locale-across-requests - I18n.with_locale(locale, &) + I18n.with_locale(safe_locale, &) end - def locale_from_params - I18n.available_locales.map(&:to_s).include?(params[:locale]) ? params[:locale] : nil + def validate_and_get_locale(locale) + return I18n.default_locale.to_s if locale.blank? + + available_locales = I18n.available_locales.map(&:to_s) + locale_without_variant = locale.split('_')[0] + + if available_locales.include?(locale) + locale + elsif available_locales.include?(locale_without_variant) + locale_without_variant + else + I18n.default_locale.to_s + end end def locale_from_account(account) return unless account - I18n.available_locales.map(&:to_s).include?(account.locale) ? account.locale : nil - end - - def locale_from_env_variable - return unless ENV.fetch('DEFAULT_LOCALE', nil) - - I18n.available_locales.map(&:to_s).include?(ENV.fetch('DEFAULT_LOCALE')) ? ENV.fetch('DEFAULT_LOCALE') : nil + account.locale end end diff --git a/app/controllers/public/api/v1/portals/articles_controller.rb b/app/controllers/public/api/v1/portals/articles_controller.rb index f0fcf403d..d07dbcb9d 100644 --- a/app/controllers/public/api/v1/portals/articles_controller.rb +++ b/app/controllers/public/api/v1/portals/articles_controller.rb @@ -6,17 +6,25 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B layout 'portal' def index - @articles = @portal.articles.published + @articles = @portal.articles.published.includes(:category, :author) @articles_count = @articles.count search_articles order_by_sort_param - @articles = @articles.page(list_params[:page]) if list_params[:page].present? + limit_results end def show; end private + def limit_results + return if list_params[:per_page].blank? + + per_page = [list_params[:per_page].to_i, 100].min + per_page = 25 if per_page < 1 + @articles = @articles.page(list_params[:page]).per(per_page) + end + def search_articles @articles = @articles.search(list_params) if list_params.present? end @@ -45,7 +53,7 @@ class Public::Api::V1::Portals::ArticlesController < Public::Api::V1::Portals::B end def list_params - params.permit(:query, :locale, :sort, :status, :page) + params.permit(:query, :locale, :sort, :status, :page, :per_page) end def permitted_params diff --git a/app/controllers/public/api/v1/portals/base_controller.rb b/app/controllers/public/api/v1/portals/base_controller.rb index f6c10f7c4..66b052b1e 100644 --- a/app/controllers/public/api/v1/portals/base_controller.rb +++ b/app/controllers/public/api/v1/portals/base_controller.rb @@ -1,4 +1,6 @@ class Public::Api::V1::Portals::BaseController < PublicController + include SwitchLocale + before_action :show_plain_layout before_action :set_color_scheme before_action :set_global_config @@ -27,14 +29,7 @@ class Public::Api::V1::Portals::BaseController < PublicController end def switch_locale_with_portal(&) - locale_without_variant = params[:locale].split('_')[0] - is_locale_available = I18n.available_locales.map(&:to_s).include?(params[:locale]) - is_locale_variant_available = I18n.available_locales.map(&:to_s).include?(locale_without_variant) - if is_locale_available - @locale = params[:locale] - elsif is_locale_variant_available - @locale = locale_without_variant - end + @locale = validate_and_get_locale(params[:locale]) I18n.with_locale(@locale, &) end @@ -44,12 +39,12 @@ class Public::Api::V1::Portals::BaseController < PublicController Rails.logger.info "Article: not found for slug: #{params[:article_slug]}" render_404 && return if article.blank? - @locale = if article.category.present? - article.category.locale - else - article.portal.default_locale - end - + article_locale = if article.category.present? + article.category.locale + else + article.portal.default_locale + end + @locale = validate_and_get_locale(article_locale) I18n.with_locale(@locale, &) end diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb index 7723e5dd2..ff5db952d 100644 --- a/app/controllers/twilio/callback_controller.rb +++ b/app/controllers/twilio/callback_controller.rb @@ -1,6 +1,6 @@ class Twilio::CallbackController < ApplicationController def create - ::Twilio::IncomingMessageService.new(params: permitted_params).perform + Webhooks::TwilioEventsJob.perform_later(permitted_params.to_unsafe_hash) head :no_content end diff --git a/app/controllers/twilio/delivery_status_controller.rb b/app/controllers/twilio/delivery_status_controller.rb index cc7afb0fc..1c756a1c2 100644 --- a/app/controllers/twilio/delivery_status_controller.rb +++ b/app/controllers/twilio/delivery_status_controller.rb @@ -1,6 +1,6 @@ class Twilio::DeliveryStatusController < ApplicationController def create - ::Twilio::DeliveryStatusService.new(params: permitted_params).perform + Webhooks::TwilioDeliveryStatusJob.perform_later(permitted_params.to_unsafe_hash) head :no_content end diff --git a/app/controllers/webhooks/whatsapp_controller.rb b/app/controllers/webhooks/whatsapp_controller.rb index 8f408d2b0..c4c376e5c 100644 --- a/app/controllers/webhooks/whatsapp_controller.rb +++ b/app/controllers/webhooks/whatsapp_controller.rb @@ -2,6 +2,12 @@ class Webhooks::WhatsappController < ActionController::API include MetaTokenVerifyConcern def process_payload + if inactive_whatsapp_number? + Rails.logger.warn("Rejected webhook for inactive WhatsApp number: #{params[:phone_number]}") + render json: { error: 'Inactive WhatsApp number' }, status: :unprocessable_entity + return + end + Webhooks::WhatsappEventsJob.perform_later(params.to_unsafe_hash) head :ok end @@ -13,4 +19,15 @@ class Webhooks::WhatsappController < ActionController::API whatsapp_webhook_verify_token = channel.provider_config['webhook_verify_token'] if channel.present? token == whatsapp_webhook_verify_token if whatsapp_webhook_verify_token.present? end + + def inactive_whatsapp_number? + phone_number = params[:phone_number] + return false if phone_number.blank? + + inactive_numbers = GlobalConfig.get_value('INACTIVE_WHATSAPP_NUMBERS').to_s + return false if inactive_numbers.blank? + + inactive_numbers_array = inactive_numbers.split(',').map(&:strip) + inactive_numbers_array.include?(phone_number) + end end diff --git a/app/javascript/dashboard/api/liveReports.js b/app/javascript/dashboard/api/liveReports.js new file mode 100644 index 000000000..1435da258 --- /dev/null +++ b/app/javascript/dashboard/api/liveReports.js @@ -0,0 +1,20 @@ +/* global axios */ +import ApiClient from './ApiClient'; + +class LiveReportsAPI extends ApiClient { + constructor() { + super('live_reports', { accountScoped: true, apiVersion: 'v2' }); + } + + getConversationMetric(params = {}) { + return axios.get(`${this.url}/conversation_metrics`, { params }); + } + + getGroupedConversations({ groupBy } = { groupBy: 'assignee_id' }) { + return axios.get(`${this.url}/grouped_conversation_metrics`, { + params: { group_by: groupBy }, + }); + } +} + +export default new LiveReportsAPI(); diff --git a/app/javascript/dashboard/assets/scss/widgets/_base.scss b/app/javascript/dashboard/assets/scss/widgets/_base.scss index 9367e8b2d..a8da23f3b 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_base.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_base.scss @@ -82,7 +82,7 @@ input[type='url']:not(.reset-base) { } input[type='file'] { - @apply bg-white dark:bg-n-solid-1 leading-[1.15] mb-4; + @apply bg-n-background leading-[1.15] mb-4; } // Select diff --git a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue index 95b368dae..4e3b96259 100644 --- a/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue +++ b/app/javascript/dashboard/components-next/dropdown-menu/DropdownMenu.vue @@ -29,6 +29,10 @@ const props = defineProps({ type: Boolean, default: false, }, + labelClass: { + type: String, + default: '', + }, }); const emit = defineEmits(['action']); @@ -97,9 +101,13 @@ onMounted(() => { {{ item.emoji }} - {{ - item.label - }} + + {{ item.label }} +
{