diff --git a/Gemfile.lock b/Gemfile.lock index 8d6132849..4fdcf804a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -170,7 +170,7 @@ GEM base64 (0.3.0) bcrypt (3.1.22) benchmark (0.4.1) - bigdecimal (3.3.1) + bigdecimal (4.1.2) bindex (0.8.1) bootsnap (1.16.0) msgpack (~> 1.2) @@ -274,8 +274,8 @@ GEM dry-logic (~> 1.5) dry-types (~> 1.8) zeitwerk (~> 2.6) - dry-types (1.8.3) - bigdecimal (~> 3.0) + dry-types (1.9.1) + bigdecimal (>= 3.0) concurrent-ruby (~> 1.0) dry-core (~> 1.0) dry-inflector (~> 1.0) @@ -598,14 +598,14 @@ GEM newrelic_rpm (9.6.0) base64 nio4r (2.7.5) - nokogiri (1.19.3) + nokogiri (1.19.4) mini_portile2 (~> 2.8.2) racc (~> 1.4) - nokogiri (1.19.3-arm64-darwin) + nokogiri (1.19.4-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.3-x86_64-darwin) + nokogiri (1.19.4-x86_64-darwin) racc (~> 1.4) - nokogiri (1.19.3-x86_64-linux-gnu) + nokogiri (1.19.4-x86_64-linux-gnu) racc (~> 1.4) oauth (1.1.6) auth-sanitizer (~> 0.2, >= 0.2.1) @@ -627,7 +627,7 @@ GEM rack (>= 1.2, < 4) snaky_hash (~> 2.0, >= 2.0.5) version_gem (~> 1.1, >= 1.1.11) - oj (3.16.10) + oj (3.17.3) bigdecimal (>= 3.0) ostruct (>= 0.2) omniauth (2.1.4) @@ -674,7 +674,7 @@ GEM opentelemetry-api (~> 1.0) orm_adapter (0.5.0) os (1.1.4) - ostruct (0.6.1) + ostruct (0.6.3) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) diff --git a/VERSION_CW b/VERSION_CW index 0fb7a35b6..fb0557132 100644 --- a/VERSION_CW +++ b/VERSION_CW @@ -1 +1 @@ -4.14.2 +4.15.1 diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index ecd6f06ea..712a24608 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -6,6 +6,11 @@ class Messages::Messenger::MessageBuilder return if unsupported_file_type?(attachment['type']) params = attachment_params(attachment) + # During Meta's sticker webhook transition, a sticker message carries both an `image` + # and a `sticker` attachment pointing to the same URL. Skip the redundant sticker so it + # isn't attached twice, while still storing legitimate duplicate attachments of other types. + return if duplicate_sticker?(attachment, params[:external_url]) + attachment_obj = @message.attachments.new(params.except(:remote_file_url)) attachment_obj.save! if facebook_reel?(attachment) @@ -13,10 +18,14 @@ class Messages::Messenger::MessageBuilder elsif params[:remote_file_url] attach_file(attachment_obj, params[:remote_file_url]) end + fetch_attachment_links(attachment_obj) + update_attachment_file_type(attachment_obj) + end + + def fetch_attachment_links(attachment_obj) fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention' fetch_ig_story_link(attachment_obj) if attachment_obj.file_type == 'ig_story' fetch_ig_post_link(attachment_obj) if attachment_obj.file_type == 'ig_post' - update_attachment_file_type(attachment_obj) end def attach_file(attachment, file_url) @@ -111,13 +120,20 @@ class Messages::Messenger::MessageBuilder # Facebook may send attachment types that don't directly match our file_type enum. # Map known aliases to their canonical enum values. - FACEBOOK_FILE_TYPE_MAP = { reel: :ig_reel }.freeze + FACEBOOK_FILE_TYPE_MAP = { reel: :ig_reel, sticker: :image }.freeze def normalize_file_type(type) sym = type.to_sym FACEBOOK_FILE_TYPE_MAP.fetch(sym, sym) end + def duplicate_sticker?(attachment, url) + return false unless attachment['type'].to_sym == :sticker + return false if url.blank? + + @message.attachments.any? { |existing| existing.external_url == url } + end + # Facebook sends reel URLs as webpage links (facebook.com/reel/...) rather than # direct video URLs. Downloading these yields HTML, not video content. def facebook_reel?(attachment) diff --git a/app/controllers/api/v1/accounts/contacts_controller.rb b/app/controllers/api/v1/accounts/contacts_controller.rb index eafda0fe2..bc1082583 100644 --- a/app/controllers/api/v1/accounts/contacts_controller.rb +++ b/app/controllers/api/v1/accounts/contacts_controller.rb @@ -214,3 +214,5 @@ class Api::V1::Accounts::ContactsController < Api::V1::Accounts::BaseController render json: error, status: error_status end end + +Api::V1::Accounts::ContactsController.prepend_mod_with('Api::V1::Accounts::ContactsController') diff --git a/app/javascript/dashboard/api/captain/messageReports.js b/app/javascript/dashboard/api/captain/messageReports.js new file mode 100644 index 000000000..2df1e5747 --- /dev/null +++ b/app/javascript/dashboard/api/captain/messageReports.js @@ -0,0 +1,9 @@ +import ApiClient from '../ApiClient'; + +class MessageReports extends ApiClient { + constructor() { + super('captain/message_reports', { accountScoped: true }); + } +} + +export default new MessageReports(); diff --git a/app/javascript/dashboard/api/helpCenter/articles.js b/app/javascript/dashboard/api/helpCenter/articles.js index bab45bcb5..55b620d1a 100644 --- a/app/javascript/dashboard/api/helpCenter/articles.js +++ b/app/javascript/dashboard/api/helpCenter/articles.js @@ -16,6 +16,7 @@ class ArticlesAPI extends PortalsAPI { authorId, categorySlug, sort, + query, }) { const url = getArticleSearchURL({ pageNumber, @@ -25,6 +26,7 @@ class ArticlesAPI extends PortalsAPI { authorId, categorySlug, sort, + query, host: this.url, }); diff --git a/app/javascript/dashboard/assets/scss/_next-colors.scss b/app/javascript/dashboard/assets/scss/_next-colors.scss index 784edce6c..4a67e5182 100644 --- a/app/javascript/dashboard/assets/scss/_next-colors.scss +++ b/app/javascript/dashboard/assets/scss/_next-colors.scss @@ -145,6 +145,12 @@ --black-alpha-2: 0, 0, 0, 0.04; --border-blue: 39, 129, 246, 0.5; --white-alpha: 255, 255, 255, 0.8; + + // Voice call widget - light mode + --call-widget: 33, 34, 38, 0.95; + --call-widget-border: 255, 255, 255, 0.1; + --call-widget-text: 237, 238, 240, 1; + --call-widget-sub-text: 173, 177, 184, 1; } .dark { @@ -291,6 +297,12 @@ --border-blue: 39, 129, 246, 0.5; --border-container: 255, 255, 255, 0; --white-alpha: 255, 255, 255, 0.1; + + // Voice call widget - dark mode + --call-widget: 50, 53, 61, 1; + --call-widget-border: 255, 255, 255, 0.07; + --call-widget-text: 237, 238, 240, 1; + --call-widget-sub-text: 173, 177, 184, 1; } } // NEXT COLORS END diff --git a/app/javascript/dashboard/components-next/Companies/CompanyCreateDialog.vue b/app/javascript/dashboard/components-next/Companies/CompanyCreateDialog.vue index b3847030b..4b9d0b466 100644 --- a/app/javascript/dashboard/components-next/Companies/CompanyCreateDialog.vue +++ b/app/javascript/dashboard/components-next/Companies/CompanyCreateDialog.vue @@ -26,6 +26,13 @@ const resetForm = () => { form.description = ''; }; +const open = (company = {}) => { + form.name = company.name || ''; + form.domain = company.domain || ''; + form.description = company.description || ''; + dialogRef.value?.open(); +}; + const handleConfirm = () => { if (isFormInvalid.value) return; @@ -45,7 +52,7 @@ const onSuccess = () => { closeDialog(); }; -defineExpose({ dialogRef, onSuccess }); +defineExpose({ dialogRef, onSuccess, open });