Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8c4b10511 | ||
|
|
48cc651a0d | ||
|
|
85284fdcc4 | ||
|
|
9a151673c2 | ||
|
|
430a4343c2 | ||
|
|
4285fb20f3 | ||
|
|
3474473e38 | ||
|
|
8275cdd7d1 | ||
|
|
7605d6c855 | ||
|
|
7b32bd7c14 | ||
|
|
c1fae81a2a | ||
|
|
839d679961 | ||
|
|
3803923ed1 | ||
|
|
523e1defff | ||
|
|
340ba9bc53 | ||
|
|
ce339d0350 | ||
|
|
1906c0ff63 |
+1
-1
@@ -553,7 +553,7 @@ GEM
|
||||
pry-rails (0.3.9)
|
||||
pry (>= 0.10.4)
|
||||
public_suffix (5.0.1)
|
||||
puma (6.4.2)
|
||||
puma (6.3.1)
|
||||
nio4r (~> 2.0)
|
||||
pundit (2.3.0)
|
||||
activesupport (>= 3.0.0)
|
||||
|
||||
@@ -8,8 +8,8 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
|
||||
def index
|
||||
@unread_count = current_user.notifications.where(account_id: current_account.id, read_at: nil).count
|
||||
@count = notifications.count
|
||||
@notifications = notifications.page(@current_page).per(RESULTS_PER_PAGE)
|
||||
@notifications = notification_finder.perform
|
||||
@count = @notifications.count
|
||||
end
|
||||
|
||||
def read_all
|
||||
@@ -61,7 +61,7 @@ class Api::V1::Accounts::NotificationsController < Api::V1::Accounts::BaseContro
|
||||
@current_page = params[:page] || 1
|
||||
end
|
||||
|
||||
def notifications
|
||||
@notifications ||= current_user.notifications.where(account_id: current_account.id)
|
||||
def notification_finder
|
||||
@notification_finder ||= NotificationFinder.new(Current.user, Current.account, params)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,9 +9,6 @@ class SuperAdmin::AppConfigsController < SuperAdmin::ApplicationController
|
||||
.map { |name, serialized_value| [name, serialized_value['value']] }
|
||||
.to_h
|
||||
# rubocop:enable Style/HashTransformValues
|
||||
@installation_configs = ConfigLoader.new.general_configs.each_with_object({}) do |config_hash, result|
|
||||
result[config_hash['name']] = config_hash.except('name')
|
||||
end
|
||||
end
|
||||
|
||||
def create
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
class NotificationFinder
|
||||
attr_reader :current_user, :current_account, :params
|
||||
|
||||
RESULTS_PER_PAGE = 15
|
||||
|
||||
def initialize(current_user, current_account, params)
|
||||
@current_user = current_user
|
||||
@current_account = current_account
|
||||
@params = params
|
||||
end
|
||||
|
||||
def perform
|
||||
set_up
|
||||
notifications
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_up
|
||||
find_all_notifications
|
||||
filter_by_notification_settings
|
||||
filter_by_status
|
||||
end
|
||||
|
||||
def find_all_notifications
|
||||
@notifications = current_user.notifications.where(account_id: @current_account.id)
|
||||
end
|
||||
|
||||
def filter_by_notification_settings
|
||||
notification_settings = current_user.notification_settings.find_by(account_id: @current_account.id)
|
||||
subscribed_notification_types = get_subscribed_notification_types(notification_settings)
|
||||
@notifications = filter_notifications_by_types(@notifications, subscribed_notification_types)
|
||||
end
|
||||
|
||||
def get_subscribed_notification_types(notification_settings)
|
||||
subscribed_notification_types = []
|
||||
|
||||
::Notification::NOTIFICATION_TYPES.each_key do |notification_type|
|
||||
email_flag = "email_#{notification_type}".to_sym
|
||||
push_flag = "push_#{notification_type}".to_sym
|
||||
|
||||
if notification_settings.public_send(email_flag) || notification_settings.public_send(push_flag)
|
||||
subscribed_notification_types << notification_type
|
||||
end
|
||||
end
|
||||
|
||||
subscribed_notification_types
|
||||
end
|
||||
|
||||
def filter_notifications_by_types(notifications, types)
|
||||
notifications.where(notification_type: types)
|
||||
end
|
||||
|
||||
def filter_by_status
|
||||
@notifications = @notifications.where('snoozed_until > ?', DateTime.now.utc) if params[:status] == 'snoozed'
|
||||
end
|
||||
|
||||
def current_page
|
||||
params[:page] || 1
|
||||
end
|
||||
|
||||
def notifications
|
||||
@notifications.page(current_page).per(RESULTS_PER_PAGE).order(last_activity_at: :desc)
|
||||
end
|
||||
end
|
||||
@@ -76,11 +76,7 @@
|
||||
|
||||
&.left {
|
||||
.bubble {
|
||||
@apply rounded-r-lg rounded-l mr-auto break-words;
|
||||
|
||||
&:not(.is-unsupported) {
|
||||
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50
|
||||
}
|
||||
@apply border border-slate-50 dark:border-slate-700 bg-white dark:bg-slate-700 text-black-900 dark:text-slate-50 rounded-r-lg rounded-l mr-auto break-words;
|
||||
|
||||
&.is-image {
|
||||
@apply rounded-lg;
|
||||
|
||||
@@ -158,6 +158,7 @@ export default {
|
||||
this.$store.dispatch('notifications/unReadCount');
|
||||
this.$store.dispatch('teams/get');
|
||||
this.$store.dispatch('attributes/get');
|
||||
this.$store.dispatch('userNotificationSettings/get');
|
||||
this.fetchCustomViews();
|
||||
},
|
||||
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<mention-box
|
||||
v-if="items.length"
|
||||
:items="items"
|
||||
@mention-select="handleMentionClick"
|
||||
>
|
||||
<mention-box :items="items" @mention-select="handleMentionClick">
|
||||
<template slot-scope="{ item }">
|
||||
<strong>{{ item.label }}</strong> - {{ item.description }}
|
||||
</template>
|
||||
|
||||
@@ -29,19 +29,8 @@
|
||||
:message-type="data.message_type"
|
||||
:parent-has-attachments="hasAttachments"
|
||||
/>
|
||||
<div v-if="isUnsupported">
|
||||
<template v-if="isAFacebookInbox && isInstagram">
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_INSTAGRAM') }}
|
||||
</template>
|
||||
<template v-else-if="isAFacebookInbox">
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE_FACEBOOK') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('CONVERSATION.UNSUPPORTED_MESSAGE') }}
|
||||
</template>
|
||||
</div>
|
||||
<bubble-text
|
||||
v-else-if="data.content"
|
||||
v-if="data.content"
|
||||
:message="message"
|
||||
:is-email="isEmailContentType"
|
||||
:display-quoted-button="displayQuotedButton"
|
||||
@@ -187,14 +176,6 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isAFacebookInbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isInstagram: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isAWhatsAppChannel: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
@@ -238,7 +219,6 @@ export default {
|
||||
this.hasAttachments ||
|
||||
this.data.content ||
|
||||
this.isEmailContentType ||
|
||||
this.isUnsupported ||
|
||||
this.isAnIntegrationMessage
|
||||
);
|
||||
},
|
||||
@@ -427,7 +407,6 @@ export default {
|
||||
return {
|
||||
bubble: this.isBubble,
|
||||
'is-private': this.data.private,
|
||||
'is-unsupported': this.isUnsupported,
|
||||
'is-image': this.hasMediaAttachment('image'),
|
||||
'is-video': this.hasMediaAttachment('video'),
|
||||
'is-text': this.hasText,
|
||||
@@ -436,9 +415,6 @@ export default {
|
||||
'is-email': this.isEmailContentType,
|
||||
};
|
||||
},
|
||||
isUnsupported() {
|
||||
return this.contentAttributes.is_unsupported ?? false;
|
||||
},
|
||||
isPending() {
|
||||
return this.data.status === MESSAGE_STATUS.PROGRESS;
|
||||
},
|
||||
@@ -450,7 +426,11 @@ export default {
|
||||
return !this.sender.type || this.sender.type === 'agent_bot';
|
||||
},
|
||||
shouldShowContextMenu() {
|
||||
return !(this.isFailed || this.isPending || this.isUnsupported);
|
||||
return !(this.isFailed || this.isPending);
|
||||
},
|
||||
errorMessage() {
|
||||
const { meta } = this.data;
|
||||
return meta ? meta.error : '';
|
||||
},
|
||||
showAvatar() {
|
||||
if (this.isOutgoing || this.isTemplate) {
|
||||
@@ -552,14 +532,6 @@ export default {
|
||||
> .bubble {
|
||||
@apply min-w-[128px];
|
||||
|
||||
&.is-unsupported {
|
||||
@apply text-xs max-w-[300px] border-dashed border border-slate-200 text-slate-600 dark:text-slate-200 bg-slate-50 dark:bg-slate-700 dark:border-slate-500;
|
||||
|
||||
.message-text--metadata .time {
|
||||
@apply text-slate-400 dark:text-slate-300;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-image,
|
||||
&.is-video {
|
||||
@apply p-0 overflow-hidden;
|
||||
@@ -572,12 +544,10 @@ export default {
|
||||
> video {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
|
||||
> video {
|
||||
@apply h-full w-full object-cover;
|
||||
}
|
||||
}
|
||||
|
||||
.video {
|
||||
@apply h-[11.25rem];
|
||||
}
|
||||
@@ -592,11 +562,9 @@ export default {
|
||||
.file--icon {
|
||||
@apply text-woot-400 dark:text-woot-400;
|
||||
}
|
||||
|
||||
.text-block-title {
|
||||
@apply text-slate-700 dark:text-slate-700;
|
||||
}
|
||||
|
||||
.download.button {
|
||||
@apply text-woot-400 dark:text-woot-400;
|
||||
}
|
||||
@@ -605,7 +573,6 @@ export default {
|
||||
&.is-private.is-text > .message-text__wrap .link {
|
||||
@apply text-woot-600 dark:text-woot-200;
|
||||
}
|
||||
|
||||
&.is-private.is-text > .message-text__wrap .prosemirror-mention-node {
|
||||
@apply font-bold bg-none rounded-sm p-0 bg-yellow-100 dark:bg-yellow-700 text-slate-700 dark:text-slate-25 underline;
|
||||
}
|
||||
@@ -616,7 +583,6 @@ export default {
|
||||
.message-text--metadata .time {
|
||||
@apply text-violet-50 dark:text-violet-50;
|
||||
}
|
||||
|
||||
&.is-private .message-text--metadata .time {
|
||||
@apply text-slate-400 dark:text-slate-400;
|
||||
}
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
:is-a-facebook-inbox="isAFacebookInbox"
|
||||
:is-instagram="isInstagramDM"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:in-reply-to="getInReplyToMessage(message)"
|
||||
/>
|
||||
@@ -56,8 +54,6 @@
|
||||
:is-a-tweet="isATweet"
|
||||
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
:is-a-facebook-inbox="isAFacebookInbox"
|
||||
:is-instagram-dm="isInstagramDM"
|
||||
:inbox-supports-reply-to="inboxSupportsReplyTo"
|
||||
:in-reply-to="getInReplyToMessage(message)"
|
||||
/>
|
||||
@@ -287,9 +283,6 @@ export default {
|
||||
unreadMessageCount() {
|
||||
return this.currentChat.unread_count || 0;
|
||||
},
|
||||
isInstagramDM() {
|
||||
return this.conversationType === 'instagram_direct_message';
|
||||
},
|
||||
inboxSupportsReplyTo() {
|
||||
const incoming = this.inboxHasFeature(INBOX_FEATURES.REPLY_TO);
|
||||
const outgoing =
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<mention-box
|
||||
v-if="items.length"
|
||||
type="variable"
|
||||
:items="items"
|
||||
@mention-select="handleVariableClick"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="items.length"
|
||||
ref="mentionsListContainer"
|
||||
class="bg-white dark:bg-slate-800 rounded-md overflow-auto absolute w-full z-20 pb-0 shadow-md left-0 bottom-full max-h-[9.75rem] border border-solid border-slate-100 dark:border-slate-700 mention--box"
|
||||
>
|
||||
|
||||
@@ -10,7 +10,6 @@ export class DataManager {
|
||||
|
||||
async initDb() {
|
||||
if (this.db) return this.db;
|
||||
const dbName = `cw-store-${this.accountId}`;
|
||||
this.db = await openDB(`cw-store-${this.accountId}`, DATA_VERSION, {
|
||||
upgrade(db) {
|
||||
db.createObjectStore('cache-keys');
|
||||
@@ -20,13 +19,6 @@ export class DataManager {
|
||||
},
|
||||
});
|
||||
|
||||
// Store the database name in LocalStorage
|
||||
const dbNames = JSON.parse(localStorage.getItem('cw-idb-names') || '[]');
|
||||
if (!dbNames.includes(dbName)) {
|
||||
dbNames.push(dbName);
|
||||
localStorage.setItem('cw-idb-names', JSON.stringify(dbNames));
|
||||
}
|
||||
|
||||
return this.db;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,9 +41,6 @@
|
||||
"SAVE_CONTACT": "Save",
|
||||
"UPLOADING_ATTACHMENTS": "Uploading attachments...",
|
||||
"REPLIED_TO_STORY": "Replied to your story",
|
||||
"UNSUPPORTED_MESSAGE": "This message is unsupported.",
|
||||
"UNSUPPORTED_MESSAGE_FACEBOOK": "This message is unsupported. You can view this message on the Facebook Messenger app.",
|
||||
"UNSUPPORTED_MESSAGE_INSTAGRAM": "This message is unsupported. You can view this message on the Instagram app.",
|
||||
"SUCCESS_DELETE_MESSAGE": "Message deleted successfully",
|
||||
"FAIL_DELETE_MESSSAGE": "Couldn't delete message! Try again",
|
||||
"NO_RESPONSE": "No response",
|
||||
|
||||
-1
@@ -50,7 +50,6 @@
|
||||
:is-loading="uiFlags.isFetching"
|
||||
:on-click-notification="openConversation"
|
||||
:in-last-page="inLastPage"
|
||||
@close="closeNotificationPanel"
|
||||
/>
|
||||
<div
|
||||
v-if="records.length !== 0"
|
||||
|
||||
-103
@@ -1,103 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<woot-button
|
||||
size="expanded"
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
@click="onClickOpenNotification()"
|
||||
>
|
||||
<div
|
||||
class="flex-row items-center p-2.5 leading-[1.4] border-b border-solid border-slate-50 dark:border-slate-700 flex w-full hover:bg-slate-75 dark:hover:bg-slate-900 hover:rounded-md"
|
||||
>
|
||||
<div
|
||||
v-if="!notificationItem.read_at"
|
||||
class="w-2 h-2 rounded-full bg-woot-500"
|
||||
/>
|
||||
<div v-else class="w-2 flex" />
|
||||
<div
|
||||
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="items-center flex">
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">
|
||||
{{
|
||||
`#${
|
||||
notificationItem.primary_actor
|
||||
? notificationItem.primary_actor.id
|
||||
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||
}`
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="text-xxs p-0.5 px-1 my-0 mx-2 bg-slate-50 dark:bg-slate-700 rounded-md"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="hasNotificationAssignee">
|
||||
<thumbnail
|
||||
:src="notificationAssigneeThumbnail"
|
||||
size="16px"
|
||||
:username="notificationAssigneeName"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex">
|
||||
<span
|
||||
class="text-slate-700 dark:text-slate-200 font-normal overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ notificationItem.push_message_title }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
|
||||
>
|
||||
{{ dynamicTime(notificationItem.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import timeMixin from 'dashboard/mixins/time';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Thumbnail,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notificationItem: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
notificationAssignee() {
|
||||
const { primary_actor: primaryActor } = this.notificationItem;
|
||||
return primaryActor?.meta?.assignee;
|
||||
},
|
||||
hasNotificationAssignee() {
|
||||
return !!this.notificationAssignee;
|
||||
},
|
||||
notificationAssigneeName() {
|
||||
return this.notificationAssignee?.name || '';
|
||||
},
|
||||
notificationAssigneeThumbnail() {
|
||||
return this.notificationAssignee?.thumbnail || '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClickOpenNotification() {
|
||||
this.$emit('open-notification', this.notificationItem);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+69
-7
@@ -1,12 +1,70 @@
|
||||
<template>
|
||||
<div class="flex-col py-2 px-2.5 overflow-auto h-full flex">
|
||||
<notification-panel-item
|
||||
<woot-button
|
||||
v-for="notificationItem in notifications"
|
||||
v-show="!isLoading"
|
||||
:key="notificationItem.id"
|
||||
:notification-item="notificationItem"
|
||||
@open-notification="onClickNotification"
|
||||
/>
|
||||
size="expanded"
|
||||
color-scheme="secondary"
|
||||
variant="link"
|
||||
@click="() => onClickNotification(notificationItem)"
|
||||
>
|
||||
<div
|
||||
class="flex-row items-center p-2.5 leading-[1.4] border-b border-solid border-slate-50 dark:border-slate-700 flex w-full hover:bg-slate-75 dark:hover:bg-slate-900 hover:rounded-md"
|
||||
>
|
||||
<div
|
||||
v-if="!notificationItem.read_at"
|
||||
class="w-2 h-2 rounded-full bg-woot-500"
|
||||
/>
|
||||
<div v-else class="w-2 flex" />
|
||||
<div
|
||||
class="flex-col ml-2.5 overflow-hidden w-full flex justify-between"
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="items-center flex">
|
||||
<span class="font-bold text-slate-800 dark:text-slate-100">
|
||||
{{
|
||||
`#${
|
||||
notificationItem.primary_actor
|
||||
? notificationItem.primary_actor.id
|
||||
: $t(`NOTIFICATIONS_PAGE.DELETE_TITLE`)
|
||||
}`
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="text-xxs p-0.5 px-1 my-0 mx-2 bg-slate-50 dark:bg-slate-700 rounded-md"
|
||||
>
|
||||
{{
|
||||
$t(
|
||||
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<thumbnail
|
||||
v-if="hasAssignee(notificationItem)"
|
||||
:src="notificationItem.primary_actor.meta.assignee.thumbnail"
|
||||
size="16px"
|
||||
:username="notificationItem.primary_actor.meta.assignee.name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex">
|
||||
<span
|
||||
class="text-slate-700 dark:text-slate-200 font-normal overflow-hidden whitespace-nowrap text-ellipsis"
|
||||
>
|
||||
{{ notificationItem.push_message_title }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
class="mt-1 text-slate-500 dark:text-slate-400 text-xxs font-semibold flex"
|
||||
>
|
||||
{{ dynamicTime(notificationItem.created_at) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</woot-button>
|
||||
<empty-state
|
||||
v-if="showEmptyResult"
|
||||
:title="$t('NOTIFICATIONS_PAGE.UNREAD_NOTIFICATION.EMPTY_MESSAGE')"
|
||||
@@ -36,16 +94,18 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import EmptyState from 'dashboard/components/widgets/EmptyState.vue';
|
||||
import NotificationPanelItem from './NotificationPanelItem.vue';
|
||||
import timeMixin from '../../../../mixins/time';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
NotificationPanelItem,
|
||||
Thumbnail,
|
||||
Spinner,
|
||||
EmptyState,
|
||||
},
|
||||
mixins: [timeMixin],
|
||||
props: {
|
||||
notifications: {
|
||||
type: Array,
|
||||
@@ -79,7 +139,9 @@ export default {
|
||||
name: 'notifications_index',
|
||||
});
|
||||
}
|
||||
this.$emit('close');
|
||||
},
|
||||
hasAssignee(notification) {
|
||||
return notification.primary_actor.meta?.assignee;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,14 +1,46 @@
|
||||
const NOTIFICATION_TYPES = {
|
||||
conversation_creation: 1,
|
||||
conversation_assignment: 2,
|
||||
assigned_conversation_new_message: 3,
|
||||
conversation_mention: 4,
|
||||
participating_conversation_new_message: 5,
|
||||
};
|
||||
|
||||
function getSubscribedNotificationTypes(pushFlags, emailFlags) {
|
||||
return Object.keys(NOTIFICATION_TYPES).filter(notificationType => {
|
||||
const emailFlag = `email_${notificationType}`;
|
||||
const pushFlag = `push_${notificationType}`;
|
||||
return pushFlags.includes(pushFlag) || emailFlags.includes(emailFlag);
|
||||
});
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
getNotifications($state) {
|
||||
return Object.values($state.records).sort((n1, n2) => n2.id - n1.id);
|
||||
getNotifications($state, __, rootState) {
|
||||
const notificationSettings =
|
||||
rootState.userNotificationSettings.record || {};
|
||||
const pushFlags = notificationSettings.selected_push_flags || {};
|
||||
const emailFlags = notificationSettings.selected_email_flags || {};
|
||||
|
||||
const subscribedNotificationTypes = getSubscribedNotificationTypes(
|
||||
pushFlags,
|
||||
emailFlags
|
||||
);
|
||||
|
||||
return Object.values($state.records)
|
||||
.filter(notification =>
|
||||
subscribedNotificationTypes.includes(notification.notification_type)
|
||||
)
|
||||
.sort((n1, n2) => n2.id - n1.id);
|
||||
},
|
||||
|
||||
getUIFlags($state) {
|
||||
return $state.uiFlags;
|
||||
},
|
||||
|
||||
getNotification: $state => id => {
|
||||
const notification = $state.records[id];
|
||||
return notification || {};
|
||||
return $state.records[id] || {};
|
||||
},
|
||||
|
||||
getMeta: $state => {
|
||||
return $state.meta;
|
||||
},
|
||||
|
||||
@@ -44,29 +44,10 @@ export const clearLocalStorageOnLogout = () => {
|
||||
};
|
||||
|
||||
export const deleteIndexedDBOnLogout = async () => {
|
||||
let dbs = [];
|
||||
try {
|
||||
dbs = await window.indexedDB.databases();
|
||||
dbs = dbs.map(db => db.name);
|
||||
} catch (e) {
|
||||
dbs = JSON.parse(localStorage.getItem('cw-idb-names') || '[]');
|
||||
}
|
||||
|
||||
dbs.forEach(dbName => {
|
||||
const deleteRequest = window.indexedDB.deleteDatabase(dbName);
|
||||
|
||||
deleteRequest.onerror = event => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Error deleting database ${dbName}.`, event);
|
||||
};
|
||||
|
||||
deleteRequest.onsuccess = () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Database ${dbName} deleted successfully.`);
|
||||
};
|
||||
const dbs = await window.indexedDB.databases();
|
||||
dbs.forEach(db => {
|
||||
window.indexedDB.deleteDatabase(db.name);
|
||||
});
|
||||
|
||||
localStorage.removeItem('cw-idb-names');
|
||||
};
|
||||
|
||||
export const clearCookiesOnLogout = () => {
|
||||
|
||||
@@ -51,9 +51,6 @@ if (window.errorLoggingConfig) {
|
||||
/safari-extension:/i,
|
||||
],
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
ignoreErrors: [
|
||||
'ResizeObserver loop completed with undelivered notifications',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,6 @@ if (window.errorLoggingConfig) {
|
||||
/safari-extension:/i,
|
||||
],
|
||||
integrations: [new Integrations.BrowserTracing()],
|
||||
ignoreErrors: [
|
||||
'ResizeObserver loop completed with undelivered notifications',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class SendOnSlackJob < MutexApplicationJob
|
||||
queue_as :medium
|
||||
retry_on LockAcquisitionError, wait: 1.second, attempts: 8
|
||||
retry_on LockAcquisitionError, wait: 1.second, attempts: 6
|
||||
|
||||
def perform(message, hook)
|
||||
with_lock(::Redis::Alfred::SLACK_MESSAGE_MUTEX, conversation_id: message.conversation_id, reference_id: hook.reference_id) do
|
||||
|
||||
@@ -8,8 +8,6 @@ class ActionCableListener < BaseListener
|
||||
end
|
||||
|
||||
def notification_deleted(event)
|
||||
return if event.data[:notification].user.blank?
|
||||
|
||||
notification, account, unread_count, count = extract_notification_and_account(event)
|
||||
tokens = [event.data[:notification].user.pubsub_token]
|
||||
broadcast(account, tokens, NOTIFICATION_DELETED, { notification: notification.push_event_data, unread_count: unread_count, count: count })
|
||||
|
||||
@@ -83,7 +83,7 @@ class ReplyMailbox < ApplicationMailbox
|
||||
end
|
||||
|
||||
def validate_resource(resource)
|
||||
raise "Email conversation with uuid: #{conversation_uuid} not found" if resource.nil?
|
||||
raise "#{resource.class.name} not found" if resource.nil?
|
||||
|
||||
resource
|
||||
end
|
||||
|
||||
@@ -7,19 +7,11 @@ class SupportMailbox < ApplicationMailbox
|
||||
:decorate_mail
|
||||
|
||||
def process
|
||||
Rails.logger.info "Processing email #{mail.message_id} from #{original_sender_email} to #{mail.to} with subject #{mail.subject}"
|
||||
|
||||
# to turn off spam conversation creation
|
||||
return unless @account.active?
|
||||
# prevent loop from chatwoot notification emails
|
||||
return if notification_email_from_chatwoot?
|
||||
|
||||
# return if email doesn't have a valid sender
|
||||
# This can happen in cases like bounce emails for invalid contact email address
|
||||
# TODO: Handle the bounce seperately and mark the contact as invalid
|
||||
# we are checking for @ since the returned value could be "\"\"" for some email clients
|
||||
return unless original_sender_email.include?('@')
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
find_or_create_contact
|
||||
find_or_create_conversation
|
||||
@@ -64,10 +56,6 @@ class SupportMailbox < ApplicationMailbox
|
||||
mail['In-Reply-To'].try(:value)
|
||||
end
|
||||
|
||||
def original_sender_email
|
||||
@processed_mail.original_sender&.downcase
|
||||
end
|
||||
|
||||
def find_or_create_conversation
|
||||
@conversation = find_conversation_by_in_reply_to || ::Conversation.create!({
|
||||
account_id: @account.id,
|
||||
@@ -86,7 +74,7 @@ class SupportMailbox < ApplicationMailbox
|
||||
end
|
||||
|
||||
def find_or_create_contact
|
||||
@contact = @inbox.contacts.find_by(email: original_sender_email)
|
||||
@contact = @inbox.contacts.find_by(email: @processed_mail.original_sender&.downcase)
|
||||
if @contact.present?
|
||||
@contact_inbox = ContactInbox.find_by(inbox: @inbox, contact: @contact)
|
||||
else
|
||||
|
||||
@@ -47,10 +47,6 @@ class Notification < ApplicationRecord
|
||||
after_create_commit :process_notification_delivery, :dispatch_create_event
|
||||
after_destroy_commit :dispatch_destroy_event
|
||||
|
||||
# TODO: Get rid of default scope
|
||||
# https://stackoverflow.com/a/1834250/939299
|
||||
default_scope { order(id: :desc) }
|
||||
|
||||
PRIMARY_ACTORS = ['Conversation'].freeze
|
||||
|
||||
def push_event_data
|
||||
|
||||
@@ -49,10 +49,8 @@ class ActionService
|
||||
end
|
||||
|
||||
def assign_team(team_ids = [])
|
||||
return unassign_team if team_ids[0]&.zero?
|
||||
# check if team belongs to account only if team_id is present
|
||||
# if team_id is nil, then it means that the team is being unassigned
|
||||
return unless !team_ids[0].nil? && team_belongs_to_account?(team_ids)
|
||||
return unassign_team if team_ids[0].zero?
|
||||
return unless team_belongs_to_account?(team_ids)
|
||||
|
||||
@conversation.update!(team_id: team_ids[0])
|
||||
end
|
||||
|
||||
@@ -100,12 +100,6 @@ class Telegram::IncomingMessageService
|
||||
def attach_files
|
||||
return unless file
|
||||
|
||||
file_download_path = inbox.channel.get_telegram_file_path(file[:file_id])
|
||||
if file_download_path.blank?
|
||||
Rails.logger.info "Telegram file download path is blank for #{file[:file_id]} : inbox_id: #{inbox.id}"
|
||||
return
|
||||
end
|
||||
|
||||
attachment_file = Down.download(
|
||||
inbox.channel.get_telegram_file_path(file[:file_id])
|
||||
)
|
||||
|
||||
@@ -107,9 +107,11 @@ class Twilio::IncomingMessageService
|
||||
def attach_files
|
||||
return if params[:MediaUrl0].blank?
|
||||
|
||||
attachment_file = download_attachment_file
|
||||
|
||||
return if attachment_file.blank?
|
||||
attachment_file = Down.download(
|
||||
params[:MediaUrl0],
|
||||
# https://support.twilio.com/hc/en-us/articles/223183748-Protect-Media-Access-with-HTTP-Basic-Authentication-for-Programmable-Messaging
|
||||
http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
|
||||
)
|
||||
|
||||
attachment = @message.attachments.new(
|
||||
account_id: @message.account_id,
|
||||
@@ -124,29 +126,4 @@ class Twilio::IncomingMessageService
|
||||
|
||||
@message.save!
|
||||
end
|
||||
|
||||
def download_attachment_file
|
||||
download_with_auth
|
||||
rescue Down::Error => e
|
||||
handle_download_attachment_error(e)
|
||||
end
|
||||
|
||||
def download_with_auth
|
||||
Down.download(
|
||||
params[:MediaUrl0],
|
||||
# https://support.twilio.com/hc/en-us/articles/223183748-Protect-Media-Access-with-HTTP-Basic-Authentication-for-Programmable-Messaging
|
||||
http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
|
||||
)
|
||||
end
|
||||
|
||||
# This is just a temporary workaround since some users have not yet enabled media protection. We will remove this in the future.
|
||||
def handle_download_attachment_error(error)
|
||||
Rails.logger.info "Error downloading attachment from Twilio: #{error.message}"
|
||||
if error.message.include?('401 Unauthorized')
|
||||
Down.download(params[:MediaUrl0])
|
||||
else
|
||||
ChatwootExceptionTracker.new(error, account: @inbox.account).capture_exception
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,26 +8,13 @@
|
||||
</header>
|
||||
<section class="main-content__body">
|
||||
<%= form_with url: super_admin_app_config_url(config: @config) , method: :post do |form| %>
|
||||
<% @allowed_configs.each do |key| %>
|
||||
<div class="flex mb-8">
|
||||
<% @allowed_configs.each do |c| %>
|
||||
<div class="field-unit">
|
||||
<div class="field-unit__label">
|
||||
<%= form.label "app_config[#{key}]", @installation_configs[key]&.dig('display_title') || key %>
|
||||
<%= form.label "app_config[#{c}]", c %>
|
||||
</div>
|
||||
<div class="field-unit__field -mt-2 ">
|
||||
<% if @installation_configs[key]&.dig('type') == 'boolean' %>
|
||||
<%= form.select "app_config[#{key}]",
|
||||
[["True", true], ["False", false]],
|
||||
{ selected: ActiveModel::Type::Boolean.new.cast(@app_config[key]) },
|
||||
class: "mt-2 border border-slate-100 p-1 rounded-md"
|
||||
%>
|
||||
<% else %>
|
||||
<%= form.text_field "app_config[#{key}]", value: @app_config[key] %>
|
||||
<% end %>
|
||||
<%if @installation_configs[key]&.dig('description').present? %>
|
||||
<p class="text-slate-400 text-xs italic pt-2">
|
||||
<%= @installation_configs[key]&.dig('description') %>
|
||||
</p>
|
||||
<% end %>
|
||||
<div class="field-unit__field">
|
||||
<%= form.text_field "app_config[#{c}]", value: @app_config[c] %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
shared: &shared
|
||||
version: '3.5.0'
|
||||
version: '3.4.0'
|
||||
|
||||
development:
|
||||
<<: *shared
|
||||
|
||||
@@ -1,46 +1,23 @@
|
||||
# if you don't specify locked attribute, the default value will be true
|
||||
# if you dont specify locked attribute, the default value will be true
|
||||
# which means the particular config will be locked
|
||||
- name: INSTALLATION_NAME
|
||||
value: 'Chatwoot'
|
||||
display_title: 'Installation Name'
|
||||
description: 'The installation wide name that would be used in the dashboard, title etc.'
|
||||
- name: LOGO_THUMBNAIL
|
||||
value: '/brand-assets/logo_thumbnail.svg'
|
||||
display_title: 'Logo Thumbnail'
|
||||
description: 'The thumbnail that would be used for favicon (512px X 512px)'
|
||||
- name: LOGO
|
||||
value: '/brand-assets/logo.svg'
|
||||
display_title: 'Logo'
|
||||
description: 'The logo that would be used on the dashboard, login page etc.'
|
||||
- name: LOGO_DARK
|
||||
value: '/brand-assets/logo_dark.svg'
|
||||
display_title: 'Logo Dark Mode'
|
||||
description: 'The logo that would be used on the dashboard, login page etc. for dark mode'
|
||||
- name: BRAND_URL
|
||||
value: 'https://www.chatwoot.com'
|
||||
display_title: 'Brand URL'
|
||||
description: 'The URL that would be used in emails under the section “Powered By”'
|
||||
- name: WIDGET_BRAND_URL
|
||||
value: 'https://www.chatwoot.com'
|
||||
display_title: 'Widget Brand URL'
|
||||
description: 'The URL that would be used in the widget under the section “Powered By”'
|
||||
- name: BRAND_NAME
|
||||
value: 'Chatwoot'
|
||||
display_title: 'Brand Name'
|
||||
description: 'The name that would be used in emails and the widget'
|
||||
- name: TERMS_URL
|
||||
value: 'https://www.chatwoot.com/terms-of-service'
|
||||
display_title: 'Terms URL'
|
||||
description: 'The terms of service URL displayed in Signup Page'
|
||||
- name: PRIVACY_URL
|
||||
value: 'https://www.chatwoot.com/privacy-policy'
|
||||
display_title: 'Privacy URL'
|
||||
description: 'The privacy policy URL displayed in the app'
|
||||
- name: DISPLAY_MANIFEST
|
||||
value: true
|
||||
display_title: 'Chatwoot Metadata'
|
||||
description: 'Display default Chatwoot metadata like favicons and upgrade warnings'
|
||||
type: boolean
|
||||
- name: MAILER_INBOUND_EMAIL_DOMAIN
|
||||
value:
|
||||
locked: false
|
||||
@@ -95,6 +72,8 @@
|
||||
value: self-hosted
|
||||
- name: CSML_EDITOR_HOST
|
||||
value:
|
||||
- name: LOGO_DARK
|
||||
value: '/brand-assets/logo_dark.svg'
|
||||
- name: INSTALLATION_PRICING_PLAN
|
||||
value: 'community'
|
||||
- name: INSTALLATION_PRICING_PLAN_QUANTITY
|
||||
|
||||
+2
-2
@@ -29,8 +29,8 @@ common: &default_settings
|
||||
# independently. If `false`, all logging-related features are disabled.
|
||||
enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_ENABLED', false) %>
|
||||
forwarding:
|
||||
# If `true`, the agent captures log records emitted by this application
|
||||
enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED', true) == "false" ? false : true %>
|
||||
# If `true`, the agent captures log records emitted by this application.
|
||||
enabled: <%= ENV.fetch('NEW_RELIC_APPLICATION_LOGGING_FORWARDING_ENABLED', true) %>
|
||||
# Defines the maximum number of log records to buffer in memory at a time.
|
||||
max_samples_stored: 30000
|
||||
metrics:
|
||||
|
||||
@@ -16,7 +16,6 @@ module Enterprise::SuperAdmin::AppConfigsController
|
||||
WIDGET_BRAND_URL
|
||||
TERMS_URL
|
||||
PRIVACY_URL
|
||||
DISPLAY_MANIFEST
|
||||
]
|
||||
else
|
||||
super
|
||||
|
||||
@@ -21,9 +21,6 @@ module Enterprise::Channelable
|
||||
|
||||
return if audited_changes.blank?
|
||||
|
||||
# skip audit log creation if the only change is whatsapp channel template update
|
||||
return if messaging_template_updates?(audited_changes)
|
||||
|
||||
Enterprise::AuditLog.create(
|
||||
auditable_id: auditable_id,
|
||||
auditable_type: auditable_type,
|
||||
@@ -33,13 +30,5 @@ module Enterprise::Channelable
|
||||
audited_changes: audited_changes
|
||||
)
|
||||
end
|
||||
|
||||
def messaging_template_updates?(changes)
|
||||
# if there is more than one key, return false
|
||||
return false unless changes.keys.length == 1
|
||||
|
||||
# if the only key is message_templates_last_updated, return true
|
||||
changes.key?('message_templates_last_updated')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -14,8 +14,7 @@ class Enterprise::Billing::HandleStripeEventService
|
||||
private
|
||||
|
||||
def process_subscription_updated
|
||||
plan = find_plan(subscription['plan']['product']) if subscription['plan'].present?
|
||||
|
||||
plan = find_plan(subscription['plan']['product'])
|
||||
# skipping self hosted plan events
|
||||
return if plan.blank? || account.blank?
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ class ChatGpt
|
||||
end
|
||||
|
||||
def initialize(context_sections = '')
|
||||
@model = 'gpt-4-1106-preview'
|
||||
@model = 'gpt-4'
|
||||
@messages = [system_message(context_sections)]
|
||||
end
|
||||
|
||||
@@ -53,7 +53,7 @@ class ChatGpt
|
||||
|
||||
def request_gpt
|
||||
headers = { 'Content-Type' => 'application/json', 'Authorization' => "Bearer #{ENV.fetch('OPENAI_API_KEY')}" }
|
||||
body = { model: @model, messages: @messages, response_format: { type: 'json_object' } }.to_json
|
||||
body = { model: @model, messages: @messages }.to_json
|
||||
Rails.logger.info "Requesting Chat GPT with body: #{body}"
|
||||
response = HTTParty.post("#{self.class.base_uri}/v1/chat/completions", headers: headers, body: body)
|
||||
Rails.logger.info "Chat GPT response: #{response.body}"
|
||||
|
||||
@@ -26,13 +26,12 @@ class ConfigLoader
|
||||
reconcile_feature_config
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def general_configs
|
||||
@config_path ||= Rails.root.join('config')
|
||||
@general_configs ||= YAML.safe_load(File.read("#{@config_path}/installation_config.yml")).freeze
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def account_features
|
||||
@account_features ||= YAML.safe_load(File.read("#{@config_path}/features.yml")).freeze
|
||||
end
|
||||
|
||||
@@ -12,7 +12,6 @@ class GlobalConfig
|
||||
config[config_key] = load_from_cache(config_key)
|
||||
end
|
||||
|
||||
typecast_config(config)
|
||||
config.with_indifferent_access
|
||||
end
|
||||
|
||||
@@ -29,14 +28,6 @@ class GlobalConfig
|
||||
|
||||
private
|
||||
|
||||
def typecast_config(config)
|
||||
general_configs = ConfigLoader.new.general_configs
|
||||
config.each do |config_key, config_value|
|
||||
config_type = general_configs.find { |c| c['name'] == config_key }&.dig('type')
|
||||
config[config_key] = ActiveRecord::Type::Boolean.new.cast(config_value) if config_type == 'boolean'
|
||||
end
|
||||
end
|
||||
|
||||
def load_from_cache(config_key)
|
||||
cache_key = "#{VERSION}:#{KEY_PREFIX}:#{config_key}"
|
||||
cached_value = $alfred.with { |conn| conn.get(cache_key) }
|
||||
|
||||
@@ -97,7 +97,7 @@ class Integrations::Slack::SendOnSlackService < Base::SendOnChannelService
|
||||
post_message if message_content.present?
|
||||
upload_file if message.attachments.any?
|
||||
rescue Slack::Web::Api::Errors::AccountInactive, Slack::Web::Api::Errors::MissingScope, Slack::Web::Api::Errors::InvalidAuth,
|
||||
Slack::Web::Api::Errors::ChannelNotFound, Slack::Web::Api::Errors::NotInChannel => e
|
||||
Slack::Web::Api::Errors::ChannelNotFound => e
|
||||
Rails.logger.error e
|
||||
hook.prompt_reauthorization!
|
||||
hook.disable
|
||||
|
||||
@@ -12,10 +12,10 @@
|
||||
# end
|
||||
#
|
||||
class Redis::LockManager
|
||||
# Default lock timeout set to 1 second. This means that if the lock isn't released
|
||||
# within 1 second, it will automatically expire.
|
||||
# Default lock timeout set to 2 seconds. This means that if the lock isn't released
|
||||
# within 2 seconds, it will automatically expire.
|
||||
# This helps to avoid deadlocks in case the process holding the lock crashes or fails to release it.
|
||||
LOCK_TIMEOUT = 1.second
|
||||
LOCK_TIMEOUT = 2.seconds
|
||||
|
||||
# Attempts to acquire a lock for the given key.
|
||||
#
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@chatwoot/chatwoot",
|
||||
"version": "3.5.0",
|
||||
"version": "3.4.0",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"eslint": "eslint app/**/*.{js,vue}",
|
||||
@@ -31,7 +31,7 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@braid/vue-formulate": "^2.5.2",
|
||||
"@chatwoot/prosemirror-schema": "1.0.5",
|
||||
"@chatwoot/prosemirror-schema": "1.0.3",
|
||||
"@chatwoot/utils": "^0.0.21",
|
||||
"@hcaptcha/vue-hcaptcha": "^0.3.2",
|
||||
"@june-so/analytics-next": "^1.36.5",
|
||||
|
||||
@@ -97,56 +97,4 @@ RSpec.describe Inbox do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'audit log with whatsapp channel' do
|
||||
let(:channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
|
||||
let(:inbox) { channel.inbox }
|
||||
|
||||
before do
|
||||
stub_request(:get, 'https://graph.facebook.com/v14.0//message_templates?access_token=test_key')
|
||||
.with(
|
||||
headers: {
|
||||
'Accept' => '*/*',
|
||||
'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3',
|
||||
'User-Agent' => 'Ruby'
|
||||
}
|
||||
)
|
||||
.to_return(status: 200, body: '', headers: {})
|
||||
end
|
||||
|
||||
context 'when inbox is created' do
|
||||
it 'has associated audit log created' do
|
||||
expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'create').count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when inbox is updated' do
|
||||
it 'has associated audit log created' do
|
||||
inbox.update(name: 'Updated Inbox')
|
||||
expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update').count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when channel is updated' do
|
||||
it 'has associated audit log created' do
|
||||
previous_phone_number = inbox.channel.phone_number
|
||||
new_phone_number = '1234567890'
|
||||
inbox.channel.update(phone_number: new_phone_number)
|
||||
|
||||
# check if channel update creates an audit log against inbox
|
||||
expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update').count).to eq(1)
|
||||
# Check for the specific phone_number update in the audit log
|
||||
expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update',
|
||||
audited_changes: { 'phone_number' => [previous_phone_number, new_phone_number] }).count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when template sync runs' do
|
||||
it 'has no associated audit log created' do
|
||||
channel.sync_templates
|
||||
# check if template sync does not create an audit log
|
||||
expect(Audited::Audit.where(auditable_type: 'Inbox', action: 'update').count).to eq(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
require 'rails_helper'
|
||||
|
||||
describe NotificationFinder do
|
||||
subject(:notification_finder) { described_class.new(user, account, params) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let!(:user) { create(:user, account: account) }
|
||||
|
||||
before do
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 1.day, last_activity_at: DateTime.now.utc + 1.day)
|
||||
create(:notification, account: account, user: user, snoozed_until: DateTime.now.utc + 3.days, updated_at: DateTime.now.utc,
|
||||
last_activity_at: DateTime.now.utc)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 2.days, last_activity_at: DateTime.now.utc + 2.days)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 4.days, last_activity_at: DateTime.now.utc + 4.days,
|
||||
notification_type: :conversation_creation)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 5.days, last_activity_at: DateTime.now.utc + 5.days,
|
||||
notification_type: :conversation_mention)
|
||||
create(:notification, account: account, user: user, updated_at: DateTime.now.utc + 6.days, last_activity_at: DateTime.now.utc + 6.days,
|
||||
notification_type: :participating_conversation_new_message)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when params are empty' do
|
||||
let(:params) { {} }
|
||||
|
||||
it 'returns notifications which aren not snoozed' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 3
|
||||
end
|
||||
|
||||
it 'orders notifications by last activity at' do
|
||||
result = notification_finder.perform
|
||||
expect(result.first.last_activity_at).to be > result.last.last_activity_at
|
||||
end
|
||||
|
||||
it 'filter notifications by user preferences' do
|
||||
notification_setting = user.notification_settings.find_by(account_id: account.id)
|
||||
notification_setting.selected_email_flags = [:email_conversation_creation]
|
||||
notification_setting.save!
|
||||
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 4
|
||||
end
|
||||
end
|
||||
|
||||
context 'when snoozed param is passed' do
|
||||
let(:params) { { status: 'snoozed' } }
|
||||
|
||||
it 'returns only snoozed notifications' do
|
||||
result = notification_finder.perform
|
||||
expect(result.length).to be 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-102
@@ -1,102 +0,0 @@
|
||||
X-Original-To: unique-id@reply.example.com
|
||||
Received: from gate.forward.smtp.example.com (mxd [192.0.2.1]) by mx.example.net with ESMTP id JANE3UihQWCm3SPLwYMiwA for <unique-id@reply.example.com>; Mon, 01 Jan 2024 08:27:12.905 +0000 (UTC)
|
||||
Return-Path: <>
|
||||
X-Virus-Scanned: OK
|
||||
Authentication-Results: smtp6.gate.example.com; iprev=pass policy.iprev="192.0.2.2"; spf=neutral smtp.mailfrom="" smtp.helo="backend.example.com"; dkim=none (message not signed) header.d=none
|
||||
X-Suspicious-Flag: NO
|
||||
X-Classification-ID: 9026a23e-a87f-11ee-b226-52540050e3e0-1-1
|
||||
Received: from [192.0.2.2] ([192.0.2.2:52052] helo=backend.example.com)
|
||||
by smtp6.gate.example.com (envelope-from <>)
|
||||
(ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384)
|
||||
id 69/60-03303-06772956; Mon, 01 Jan 2024 03:27:12 -0500
|
||||
Received: by backend.example.com (Postfix, from userid 5000)
|
||||
id BE58147CF5; Mon, 1 Jan 2024 03:27:12 -0500 (EST)
|
||||
X-Sieve: Pigeonhole Sieve 0.5.12 (f22f7ab3)
|
||||
X-Sieve-Redirected-From: support@example.com
|
||||
Delivered-To: support@example.com
|
||||
Delivered-To: support@example.com
|
||||
Received: from director.example.com ([192.0.2.3])
|
||||
by backend.example.com with LMTP
|
||||
id AB5kLWB3kmV0bgAAStNUoA
|
||||
(envelope-from <>)
|
||||
for <support@example.com>; Mon, 01 Jan 2024 03:27:12 -0500
|
||||
Received: from proxy.example.com ([192.0.2.3])
|
||||
by director.example.com with LMTP
|
||||
id 0BPqLGB3kmWXKQAAfY0hYg
|
||||
(envelope-from <>)
|
||||
for <support@example.com>; Mon, 01 Jan 2024 03:27:12 -0500
|
||||
Received: from smtp.example.com ([192.0.2.3])
|
||||
(using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits))
|
||||
by proxy.example.com with LMTPS
|
||||
id yGPPLGB3kmXQNwAAyH2SIw
|
||||
(envelope-from <>)
|
||||
for <support@example.com>; Mon, 01 Jan 2024 03:27:12 -0500
|
||||
X-Spam-Threshold: 95
|
||||
X-Spam-Score: 0
|
||||
X-Spam-Flag: NO
|
||||
X-Virus-Scanned: OK
|
||||
X-Orig-To: support@example.com
|
||||
X-Originating-Ip: [192.0.2.4]
|
||||
Received: from [192.0.2.4] ([192.0.2.4:47194] helo=smtp.example.com)
|
||||
by smtp36.gate.example.com (envelope-from <>)
|
||||
(ecelerity 4.2.38.62370 r(:)) with ESMTPS (cipher=DHE-RSA-AES256-GCM-SHA384)
|
||||
id 57/13-02844-06772956; Mon, 01 Jan 2024 03:27:12 -0500
|
||||
Received: by smtp5.relay.example.com (SMTP Server)
|
||||
id 8D329A008A; Mon, 1 Jan 2024 03:27:12 -0500 (EST)
|
||||
Date: Mon, 1 Jan 2024 03:27:12 -0500 (EST)
|
||||
From: "" (Mail Delivery System)
|
||||
Subject: Undelivered Mail Returned to Sender
|
||||
To: support@example.com
|
||||
Auto-Submitted: auto-replied
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/report; report-type=delivery-status;
|
||||
boundary="AE732A0081.1704097632/smtp5.relay.example.com"
|
||||
Message-Id: <20240101082712.8D329A008A@smtp5.relay.example.com>
|
||||
|
||||
This is a MIME-encapsulated message.
|
||||
|
||||
--AE732A0081.1704097632/smtp5.relay.example.com
|
||||
Content-Description: Notification
|
||||
Content-Type: text/plain; charset=us-ascii
|
||||
|
||||
This is the mail system at host smtp5.relay.example.com.
|
||||
|
||||
I'm sorry to have to inform you that your message could not
|
||||
be delivered to one or more recipients. It's attached below.
|
||||
|
||||
For further assistance, please send mail to postmaster.
|
||||
|
||||
If you do so, please include this problem report. You can
|
||||
delete your own text from the attached returned message.
|
||||
|
||||
The mail system
|
||||
|
||||
<noreply@example-service.com>: host
|
||||
mx.example-service.com.cust.a.hostedemail.com[198.51.100.4] said: 554 5.7.1
|
||||
<noreply@example-service.com>: Recipient address rejected: user
|
||||
noreply@example-service.com does not exist (in reply to RCPT TO command)
|
||||
|
||||
--AE732A0081.1704097632/smtp5.relay.example.com
|
||||
Content-Description: Delivery report
|
||||
Content-Type: message/delivery-status
|
||||
|
||||
Reporting-MTA: dns; smtp5.relay.example.com
|
||||
X-SMTP-Server-Queue-ID: AE732A0081
|
||||
X-SMTP-Server-Sender: rfc822; support@example.com
|
||||
Arrival-Date: Mon, 1 Jan 2024 03:27:11 -0500 (EST)
|
||||
|
||||
Final-Recipient: rfc822; noreply@example-service.com
|
||||
Original-Recipient: rfc822;noreply@example-service.com
|
||||
Action: failed
|
||||
Status: 5.7.1
|
||||
Remote-MTA: dns; mx.example-service.com.cust.a.hostedemail.com
|
||||
Diagnostic-Code: smtp; 554 5.7.1 <noreply@example-service.com>: Recipient
|
||||
address rejected: user noreply@example-service.com does not exist
|
||||
|
||||
--AE732A0081.1704097632/smtp5.relay.example.com
|
||||
Content-Description: Undelivered Message
|
||||
Content-Type: message/rfc822
|
||||
|
||||
Return-Path: <support@example.com>
|
||||
X-Milter-Dummy:
|
||||
DKIM-Signature: v=1; a=rsa-sha256; c
|
||||
@@ -42,11 +42,6 @@ describe Redis::Config do
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
# ensuring the redis config is unset and won't affect other tests
|
||||
described_class.instance_variable_set(:@config, nil)
|
||||
end
|
||||
|
||||
it 'checks for app redis config' do
|
||||
expect(described_class.app.keys).to contain_exactly(:url, :password, :sentinels, :timeout, :reconnect_attempts, :ssl_params)
|
||||
expect(described_class.app[:url]).to eq("redis://#{redis_master_name}")
|
||||
@@ -64,11 +59,6 @@ describe Redis::Config do
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
# ensuring the redis config is unset and won't affect other tests
|
||||
described_class.instance_variable_set(:@config, nil)
|
||||
end
|
||||
|
||||
it 'checks for app redis config and sentinel passwords will be empty' do
|
||||
expect(described_class.app.keys).to contain_exactly(:url, :password, :sentinels, :timeout, :reconnect_attempts, :ssl_params)
|
||||
expect(described_class.app[:url]).to eq("redis://#{redis_master_name}")
|
||||
@@ -87,11 +77,6 @@ describe Redis::Config do
|
||||
end
|
||||
end
|
||||
|
||||
after do
|
||||
# ensuring the redis config is unset and won't affect other tests
|
||||
described_class.instance_variable_set(:@config, nil)
|
||||
end
|
||||
|
||||
it 'checks for app redis config and redis password is replaced in sentinel config' do
|
||||
expect(described_class.app.keys).to contain_exactly(:url, :password, :sentinels, :timeout, :reconnect_attempts, :ssl_params)
|
||||
expect(described_class.app[:url]).to eq("redis://#{redis_master_name}")
|
||||
|
||||
@@ -16,17 +16,6 @@ RSpec.describe SupportMailbox do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when bounced email with out a sender is recieved' do
|
||||
let(:account) { create(:account) }
|
||||
let(:bounced_email) { create_inbound_email_from_fixture('bounced_with_no_from.eml') }
|
||||
let(:described_subject) { described_class.receive bounced_email }
|
||||
|
||||
it 'shouldnt throw an error' do
|
||||
create(:channel_email, email: 'support@example.com', account: account)
|
||||
expect { described_subject }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when an account is suspended' do
|
||||
let(:account) { create(:account, status: :suspended) }
|
||||
let(:agent) { create(:user, email: 'agent1@example.com', account: account) }
|
||||
|
||||
@@ -39,10 +39,5 @@ describe ActionService do
|
||||
action_service.assign_agent(['nil'])
|
||||
expect(conversation.reload.assignee).to be_nil
|
||||
end
|
||||
|
||||
it 'unassigns the team if team_id is nil' do
|
||||
action_service.assign_team([nil])
|
||||
expect(conversation.reload.team).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -215,26 +215,6 @@ describe Telegram::IncomingMessageService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the API call to get the download path returns an error' do
|
||||
it 'does not process the attachment' do
|
||||
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return(nil)
|
||||
params = {
|
||||
'update_id' => 2_342_342_343_242,
|
||||
'message' => {
|
||||
'document' => {
|
||||
'file_id' => 'AwADBAADbXXXXXXXXXXXGBdhD2l6_XX',
|
||||
'file_name' => 'Screenshot 2021-09-27 at 2.01.14 PM.png',
|
||||
'mime_type' => 'application/png',
|
||||
'file_size' => 536_392
|
||||
}
|
||||
}.merge(message_params)
|
||||
}.with_indifferent_access
|
||||
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.messages.first.attachments.count).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid location message params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
params = {
|
||||
|
||||
@@ -169,65 +169,5 @@ describe Twilio::IncomingMessageService do
|
||||
expect(twilio_sms_channel.inbox.conversations.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a message with an attachment is received' do
|
||||
before do
|
||||
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
|
||||
.to_return(status: 200, body: 'image data', headers: {})
|
||||
end
|
||||
|
||||
let(:params_with_attachment) do
|
||||
{
|
||||
SmsSid: 'SMxx',
|
||||
From: '+12345',
|
||||
AccountSid: 'ACxxx',
|
||||
MessagingServiceSid: twilio_channel.messaging_service_sid,
|
||||
Body: 'testing3',
|
||||
NumMedia: '1',
|
||||
MediaContentType0: 'image/jpeg',
|
||||
MediaUrl0: 'https://chatwoot-assets.local/sample.png'
|
||||
}
|
||||
end
|
||||
|
||||
it 'creates a new message with media in existing conversation' do
|
||||
described_class.new(params: params_with_attachment).perform
|
||||
expect(conversation.reload.messages.last.content).to eq('testing3')
|
||||
expect(conversation.reload.messages.last.attachments.count).to eq(1)
|
||||
expect(conversation.reload.messages.last.attachments.first.file_type).to eq('image')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there is an error downloading the attachment' do
|
||||
before do
|
||||
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
|
||||
.to_raise(Down::Error.new('Download error'))
|
||||
|
||||
stub_request(:get, 'https://chatwoot-assets.local/sample.png')
|
||||
.to_return(status: 200, body: 'image data', headers: {})
|
||||
end
|
||||
|
||||
let(:params_with_attachment_error) do
|
||||
{
|
||||
SmsSid: 'SMxx',
|
||||
From: '+12345',
|
||||
AccountSid: 'ACxxx',
|
||||
MessagingServiceSid: twilio_channel.messaging_service_sid,
|
||||
Body: 'testing3',
|
||||
NumMedia: '1',
|
||||
MediaContentType0: 'image/jpeg',
|
||||
MediaUrl0: 'https://chatwoot-assets.local/sample.png'
|
||||
}
|
||||
end
|
||||
|
||||
it 'retries downloading the attachment without a token after an error' do
|
||||
expect do
|
||||
described_class.new(params: params_with_attachment_error).perform
|
||||
end.not_to raise_error
|
||||
|
||||
expect(conversation.reload.messages.last.content).to eq('testing3')
|
||||
expect(conversation.reload.messages.last.attachments.count).to eq(1)
|
||||
expect(conversation.reload.messages.last.attachments.first.file_type).to eq('image')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3156,10 +3156,10 @@
|
||||
"@braid/vue-formulate-i18n" "^1.16.0"
|
||||
is-plain-object "^3.0.1"
|
||||
|
||||
"@chatwoot/prosemirror-schema@1.0.5":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.5.tgz#d6053692beae59d466ac0b04128fa157f59eb176"
|
||||
integrity sha512-dOzkZ2K53PPbE9AQB0RHlVs+GIEyHHdXeeW44dNSEuULwH99PmTpzA2r45QX3uaVa2j7Mip76AQbJZGKbM2fxg==
|
||||
"@chatwoot/prosemirror-schema@1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@chatwoot/prosemirror-schema/-/prosemirror-schema-1.0.3.tgz#8599e517d42cb31fabf977554ade666eb6316ef0"
|
||||
integrity sha512-BIVxV7c8x0vbWtWxGPk/VnBrtC1CV0TzZd+GPZC49irVcQQ2vAwgOYaZ/1qcFe9M3jv0kWAWOPqQAfbB5RBB7g==
|
||||
dependencies:
|
||||
markdown-it-sup "^1.0.0"
|
||||
prosemirror-commands "^1.1.4"
|
||||
|
||||
Reference in New Issue
Block a user