diff --git a/app/controllers/api/v1/accounts/portals_controller.rb b/app/controllers/api/v1/accounts/portals_controller.rb
index 18538e842..af96441f8 100644
--- a/app/controllers/api/v1/accounts/portals_controller.rb
+++ b/app/controllers/api/v1/accounts/portals_controller.rb
@@ -26,9 +26,8 @@ class Api::V1::Accounts::PortalsController < Api::V1::Accounts::BaseController
@portal.update!(portal_params.merge(live_chat_widget_params)) if params[:portal].present?
# @portal.custom_domain = parsed_custom_domain
process_attached_logo if params[:blob_id].present?
- rescue StandardError => e
- Rails.logger.error e
- render json: { error: @portal.errors.messages }.to_json, status: :unprocessable_entity
+ rescue ActiveRecord::RecordInvalid => e
+ render_record_invalid(e)
end
end
diff --git a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
index e7a1f3fa6..3e7d876c3 100644
--- a/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
+++ b/app/controllers/api/v1/accounts/whatsapp/authorizations_controller.rb
@@ -1,8 +1,10 @@
class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts::BaseController
before_action :validate_feature_enabled!
+ before_action :fetch_and_validate_inbox, if: -> { params[:inbox_id].present? }
# POST /api/v1/accounts/:account_id/whatsapp/authorization
- # Handles the embedded signup callback data from the Facebook SDK
+ # Handles both initial authorization and reauthorization
+ # If inbox_id is present in params, it performs reauthorization
def create
validate_embedded_signup_params!
channel = process_embedded_signup
@@ -16,21 +18,42 @@ class Api::V1::Accounts::Whatsapp::AuthorizationsController < Api::V1::Accounts:
def process_embedded_signup
service = Whatsapp::EmbeddedSignupService.new(
account: Current.account,
- code: params[:code],
- business_id: params[:business_id],
- waba_id: params[:waba_id],
- phone_number_id: params[:phone_number_id]
+ params: params.permit(:code, :business_id, :waba_id, :phone_number_id).to_h.symbolize_keys,
+ inbox_id: params[:inbox_id]
)
service.perform
end
- def render_success_response(inbox)
+ def fetch_and_validate_inbox
+ @inbox = Current.account.inboxes.find(params[:inbox_id])
+ validate_reauthorization_required
+ end
+
+ def validate_reauthorization_required
+ return if @inbox.channel.reauthorization_required? || can_upgrade_to_embedded_signup?
+
render json: {
+ success: false,
+ message: I18n.t('inbox.reauthorization.not_required')
+ }, status: :unprocessable_entity
+ end
+
+ def can_upgrade_to_embedded_signup?
+ channel = @inbox.channel
+ return false unless channel.provider == 'whatsapp_cloud'
+
+ true
+ end
+
+ def render_success_response(inbox)
+ response = {
success: true,
id: inbox.id,
name: inbox.name,
channel_type: 'whatsapp'
}
+ response[:message] = I18n.t('inbox.reauthorization.success') if params[:inbox_id].present?
+ render json: response
end
def render_error_response(error)
diff --git a/app/controllers/slack_uploads_controller.rb b/app/controllers/slack_uploads_controller.rb
index 127e77649..6f157c7d5 100644
--- a/app/controllers/slack_uploads_controller.rb
+++ b/app/controllers/slack_uploads_controller.rb
@@ -17,7 +17,12 @@ class SlackUploadsController < ApplicationController
end
def blob_url
- url_for(@blob.representation(resize_to_fill: [250, nil]))
+ # Only generate representations for images
+ if @blob.content_type.start_with?('image/')
+ url_for(@blob.representation(resize_to_fill: [250, nil]))
+ else
+ url_for(@blob)
+ end
end
def avatar_url
diff --git a/app/controllers/twilio/callback_controller.rb b/app/controllers/twilio/callback_controller.rb
index 455828228..d607ba151 100644
--- a/app/controllers/twilio/callback_controller.rb
+++ b/app/controllers/twilio/callback_controller.rb
@@ -30,7 +30,8 @@ class Twilio::CallbackController < ApplicationController
:NumMedia,
:Latitude,
:Longitude,
- :MessageType
+ :MessageType,
+ :ProfileName
)
end
end
diff --git a/app/finders/notification_finder.rb b/app/finders/notification_finder.rb
index ccfe470a0..e1958827a 100644
--- a/app/finders/notification_finder.rb
+++ b/app/finders/notification_finder.rb
@@ -15,7 +15,13 @@ class NotificationFinder
end
def unread_count
- @notifications.where(read_at: nil).count
+ if type_included?('read')
+ # If we're including read notifications, filter to unread
+ @notifications.where(read_at: nil).count
+ else
+ # Already filtered to unread notifications, just count
+ @notifications.count
+ end
end
def count
@@ -27,7 +33,7 @@ class NotificationFinder
def set_up
find_all_notifications
filter_snoozed_notifications
- fitler_read_notifications
+ filter_read_notifications
end
def find_all_notifications
@@ -38,7 +44,7 @@ class NotificationFinder
@notifications = @notifications.where(snoozed_until: nil) unless type_included?('snoozed')
end
- def fitler_read_notifications
+ def filter_read_notifications
@notifications = @notifications.where(read_at: nil) unless type_included?('read')
end
diff --git a/app/javascript/dashboard/api/channel/whatsappChannel.js b/app/javascript/dashboard/api/channel/whatsappChannel.js
index e1003b123..8f51f4878 100644
--- a/app/javascript/dashboard/api/channel/whatsappChannel.js
+++ b/app/javascript/dashboard/api/channel/whatsappChannel.js
@@ -9,6 +9,13 @@ class WhatsappChannel extends ApiClient {
createEmbeddedSignup(params) {
return axios.post(`${this.baseUrl()}/whatsapp/authorization`, params);
}
+
+ reauthorizeWhatsApp({ inboxId, ...params }) {
+ return axios.post(`${this.baseUrl()}/whatsapp/authorization`, {
+ ...params,
+ inbox_id: inboxId,
+ });
+ }
}
export default new WhatsappChannel();
diff --git a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
index 304e55347..05507fc89 100644
--- a/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/CampaignCard/CampaignCard.vue
@@ -76,8 +76,8 @@ const campaignStatus = computed(() => {
const inboxName = computed(() => props.inbox?.name || '');
const inboxIcon = computed(() => {
- const { phone_number: phoneNumber, channel_type: type } = props.inbox;
- return getInboxIconByType(type, phoneNumber);
+ const { medium, channel_type: type } = props.inbox;
+ return getInboxIconByType(type, medium);
});
diff --git a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
index 12a789fee..f4301375d 100644
--- a/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
+++ b/app/javascript/dashboard/components-next/Campaigns/Pages/CampaignPage/WhatsAppCampaign/WhatsAppCampaignDialog.vue
@@ -38,11 +38,13 @@ const handleClose = () => emit('close');
-
- {{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
-
-
+
+
+ {{ t(`CAMPAIGN.WHATSAPP.CREATE.TITLE`) }}
+
+
+
diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue
index 0932a79c7..0e893b767 100644
--- a/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue
+++ b/app/javascript/dashboard/components-next/Contacts/ContactsCard/ContactsCard.vue
@@ -98,6 +98,7 @@ const onClickViewDetails = () => emit('showContact', props.id);
:src="thumbnail"
:size="48"
:status="availabilityStatus"
+ hide-offline-status
rounded-full
/>
diff --git a/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue b/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue
index 36f611775..f9a2507a1 100644
--- a/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue
+++ b/app/javascript/dashboard/components-next/Conversation/ConversationCard/ConversationCard.vue
@@ -48,8 +48,8 @@ const inbox = computed(() => props.stateInbox);
const inboxName = computed(() => inbox.value?.name);
const inboxIcon = computed(() => {
- const { phoneNumber, channelType } = inbox.value;
- return getInboxIconByType(channelType, phoneNumber);
+ const { channelType, medium } = inbox.value;
+ return getInboxIconByType(channelType, medium);
});
const lastActivityAt = computed(() => {
diff --git a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
index 7995270b5..862a2cd67 100644
--- a/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
+++ b/app/javascript/dashboard/components-next/HelpCenter/Pages/PortalSettingsPage/PortalBaseSettings.vue
@@ -7,8 +7,8 @@ import { useStore, useStoreGetters } from 'dashboard/composables/store';
import { uploadFile } from 'dashboard/helper/uploadHelper';
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
import { useVuelidate } from '@vuelidate/core';
-import { required, minLength, helpers } from '@vuelidate/validators';
-import { shouldBeUrl, isValidSlug } from 'shared/helpers/Validators';
+import { required, minLength, helpers, url } from '@vuelidate/validators';
+import { isValidSlug } from 'shared/helpers/Validators';
import Button from 'dashboard/components-next/button/Button.vue';
import Input from 'dashboard/components-next/input/Input.vue';
@@ -71,7 +71,7 @@ const rules = {
isValidSlug
),
},
- homePageLink: { shouldBeUrl },
+ homePageLink: { url },
};
const v$ = useVuelidate(rules, state);
diff --git a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
index 79a451404..958f25048 100644
--- a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
+++ b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue
@@ -49,8 +49,8 @@ const isUnread = computed(() => !props.inboxItem?.readAt);
const inbox = computed(() => props.stateInbox);
const inboxIcon = computed(() => {
- const { phoneNumber, channelType } = inbox.value;
- return getInboxIconByType(channelType, phoneNumber);
+ const { channelType, medium } = inbox.value;
+ return getInboxIconByType(channelType, medium);
});
const hasSlaThreshold = computed(() => {
@@ -63,11 +63,12 @@ const lastActivityAt = computed(() => {
});
const menuItems = computed(() => [
- { key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
{
key: isUnread.value ? 'mark_as_read' : 'mark_as_unread',
+ icon: isUnread.value ? 'mail' : 'mail-unread',
label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`),
},
+ { key: 'delete', icon: 'delete', label: t('INBOX.MENU_ITEM.DELETE') },
]);
const messageClasses = computed(() => ({
diff --git a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
index 57e819245..cb79752f7 100644
--- a/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
+++ b/app/javascript/dashboard/components-next/NewConversation/helpers/composeConversationHelper.js
@@ -36,10 +36,11 @@ const transformInbox = ({
email,
channelType,
phoneNumber,
+ medium,
...rest
}) => ({
id,
- icon: getInboxIconByType(channelType, phoneNumber, 'line'),
+ icon: getInboxIconByType(channelType, medium, 'line'),
label: generateLabelForContactableInboxesList({
name,
email,
diff --git a/app/javascript/dashboard/components-next/avatar/Avatar.vue b/app/javascript/dashboard/components-next/avatar/Avatar.vue
index 65859b1c7..a47566566 100644
--- a/app/javascript/dashboard/components-next/avatar/Avatar.vue
+++ b/app/javascript/dashboard/components-next/avatar/Avatar.vue
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
import { removeEmoji } from 'shared/helpers/emoji';
import Icon from 'dashboard/components-next/icon/Icon.vue';
+import ChannelIcon from 'dashboard/components-next/icon/ChannelIcon.vue';
import wootConstants from 'dashboard/constants/globals';
const props = defineProps({
@@ -33,10 +34,18 @@ const props = defineProps({
validator: value =>
!value || wootConstants.AVAILABILITY_STATUS_KEYS.includes(value),
},
+ inbox: {
+ type: Object,
+ default: null,
+ },
iconName: {
type: String,
default: null,
},
+ hideOfflineStatus: {
+ type: Boolean,
+ default: false,
+ },
});
const emit = defineEmits(['upload', 'delete']);
@@ -66,11 +75,11 @@ const AVATAR_COLORS = {
default: { bg: '#E8E8E8', text: '#60646C' },
};
-const STATUS_CLASSES = {
+const STATUS_CLASSES = computed(() => ({
online: 'bg-n-teal-10',
busy: 'bg-n-amber-10',
- offline: 'bg-n-slate-10',
-};
+ ...(props.hideOfflineStatus ? {} : { offline: 'bg-n-slate-10' }),
+}));
const showDefaultAvatar = computed(() => !props.src && !props.name);
@@ -174,15 +183,25 @@ watch(
-
+
+
+
+
@@ -232,31 +251,40 @@ watch(
-
-
-
-
+
+
+
+
+
diff --git a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
index 30fe85b0a..f349720b0 100644
--- a/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
+++ b/app/javascript/dashboard/components-next/captain/assistant/InboxCard.vue
@@ -57,9 +57,10 @@ const menuItems = computed(() => [
},
]);
-const icon = computed(() =>
- getInboxIconByType(props.inbox.channel_type, '', 'outline')
-);
+const icon = computed(() => {
+ const { medium, channel_type: type } = props.inbox;
+ return getInboxIconByType(type, medium, 'outline');
+});
const handleAction = ({ action, value }) => {
toggleDropdown(false);
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
index 8c26de2f5..ecf13c286 100644
--- a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/AssistantForm.vue
@@ -35,6 +35,7 @@ const initialState = {
productName: '',
featureFaq: false,
featureMemory: false,
+ featureCitation: false,
};
const state = reactive({ ...initialState });
@@ -70,6 +71,7 @@ const prepareAssistantDetails = () => ({
product_name: state.productName,
feature_faq: state.featureFaq,
feature_memory: state.featureMemory,
+ feature_citation: state.featureCitation,
},
});
@@ -93,6 +95,7 @@ const updateStateFromAssistant = assistant => {
productName: config.product_name,
featureFaq: config.feature_faq || false,
featureMemory: config.feature_memory || false,
+ featureCitation: config.feature_citation || false,
});
};
@@ -151,6 +154,13 @@ watch(
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
+
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
index 3134d7062..ad65f7511 100644
--- a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/EditAssistantForm.vue
@@ -41,6 +41,7 @@ const initialState = {
features: {
conversationFaqs: false,
memories: false,
+ citations: false,
},
temperature: 1,
};
@@ -87,6 +88,7 @@ const updateStateFromAssistant = assistant => {
state.features = {
conversationFaqs: config.feature_faq || false,
memories: config.feature_memory || false,
+ citations: config.feature_citation || false,
};
state.temperature = config.temperature || 1;
};
@@ -152,6 +154,7 @@ const handleFeaturesUpdate = () => {
...props.assistant.config,
feature_faq: state.features.conversationFaqs,
feature_memory: state.features.memories,
+ feature_citation: state.features.citations,
},
};
@@ -314,6 +317,14 @@ watch(
/>
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
+
diff --git a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue
index 236791793..faaf0a825 100644
--- a/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue
+++ b/app/javascript/dashboard/components-next/captain/pageComponents/assistant/settings/AssistantBasicSettingsForm.vue
@@ -26,6 +26,7 @@ const initialState = {
features: {
conversationFaqs: false,
memories: false,
+ citations: false,
},
};
@@ -57,6 +58,7 @@ const updateStateFromAssistant = assistant => {
state.features = {
conversationFaqs: config.feature_faq || false,
memories: config.feature_memory || false,
+ citations: config.feature_citation || false,
};
};
@@ -76,6 +78,7 @@ const handleBasicInfoUpdate = async () => {
product_name: state.productName,
feature_faq: state.features.conversationFaqs,
feature_memory: state.features.memories,
+ feature_citation: state.features.citations,
},
};
@@ -138,6 +141,14 @@ watch(
/>
{{ t('CAPTAIN.ASSISTANTS.FORM.FEATURES.ALLOW_MEMORIES') }}
+