Skip to content

Commit a703284

Browse files
committed
Merge branch 'feat-provider-overrides' into add-fcm-topics
2 parents 1dd8eb7 + debb52b commit a703284

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+2525
-3221
lines changed

.source

apps/api/src/app/inbox/usecases/notifications-count/notifications-count.spec.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ describe('NotificationsCount', () => {
1515
let notificationsCount: NotificationsCount;
1616
let messageRepository: sinon.SinonStubbedInstance<MessageRepository>;
1717
let subscriberRepository: sinon.SinonStubbedInstance<SubscriberRepository>;
18-
let organizationRepository: sinon.SinonStubbedInstance<OrganizationRepository>;
1918

2019
beforeEach(() => {
2120
messageRepository = sinon.createStubInstance(MessageRepository);
@@ -177,6 +176,40 @@ describe('NotificationsCount', () => {
177176
)
178177
).to.be.true;
179178

179+
await notificationsCount.execute({
180+
organizationId: 'organizationId',
181+
environmentId,
182+
subscriberId: 'subscriber-id',
183+
filters: [{ snoozed: true }],
184+
});
185+
186+
expect(
187+
messageRepository.getCount.calledWith(
188+
environmentId,
189+
subscriber._id,
190+
ChannelTypeEnum.IN_APP,
191+
{ snoozed: true },
192+
{ limit: 99 }
193+
)
194+
).to.be.true;
195+
196+
await notificationsCount.execute({
197+
organizationId: 'organizationId',
198+
environmentId,
199+
subscriberId: 'subscriber-id',
200+
filters: [{ snoozed: false }],
201+
});
202+
203+
expect(
204+
messageRepository.getCount.calledWith(
205+
environmentId,
206+
subscriber._id,
207+
ChannelTypeEnum.IN_APP,
208+
{ snoozed: false },
209+
{ limit: 99 }
210+
)
211+
).to.be.true;
212+
180213
await notificationsCount.execute({
181214
organizationId: 'organizationId',
182215
environmentId,

apps/dashboard/src/components/integrations/components/integration-card.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ function InAppPremiumFeaturesIcon() {
158158
<TooltipContent>
159159
<div className="flex flex-col gap-2">
160160
<div>
161-
Upgrade to remove the 'Inbox by Novu' badge and enable notification reminders in your{' '}
161+
Upgrade to remove the 'Inbox by Novu' badge and extend notification snooze beyond 24 hours in your{' '}
162162
<span className="font-mono">{`<Inbox />`}</span> component.
163163
</div>
164164
<button onClick={handleUpgradeClick} className="flex items-center gap-1 text-xs font-medium hover:underline">

apps/dashboard/src/components/integrations/components/integration-general-settings.tsx

+1-78
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import { Input } from '@/components/primitives/input';
44
import { Popover, PopoverContent, PopoverTrigger } from '@/components/primitives/popover';
55
import { Separator } from '@/components/primitives/separator';
66
import { Switch } from '@/components/primitives/switch';
7-
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/primitives/tooltip';
8-
import { useFeatureFlag } from '@/hooks/use-feature-flag';
97
import { useFetchSubscription } from '@/hooks/use-fetch-subscription';
108
import { ROUTES } from '@/utils/routes';
11-
import { ApiServiceLevelEnum, FeatureFlagsKeysEnum } from '@novu/shared';
9+
import { ApiServiceLevelEnum } from '@novu/shared';
1210
import { Control } from 'react-hook-form';
1311
import { useNavigate } from 'react-router-dom';
1412

@@ -21,7 +19,6 @@ type IntegrationFormData = {
2119
primary: boolean;
2220
environmentId: string;
2321
removeNovuBranding?: boolean;
24-
enableSnooze?: boolean;
2522
};
2623

2724
type GeneralSettingsProps = {
@@ -32,56 +29,6 @@ type GeneralSettingsProps = {
3229
isForInAppStep?: boolean;
3330
};
3431

35-
/**
36-
* This switch doesn't actually set any value, it serves as an indicator
37-
* informing if the feature is enabled or not.
38-
*/
39-
function EnableSnoozeSwitch({ id }: { id: string }) {
40-
const { subscription, isLoading } = useFetchSubscription();
41-
const navigate = useNavigate();
42-
const isFreePlan = subscription?.apiServiceLevel === ApiServiceLevelEnum.FREE;
43-
const disabled = isFreePlan || isLoading;
44-
const checked = disabled ? false : true; // Always checked for paid plans
45-
46-
return (
47-
<div className="flex items-center">
48-
{isFreePlan ? (
49-
<Popover modal>
50-
<PopoverTrigger asChild>
51-
<Switch id={id} checked={checked} />
52-
</PopoverTrigger>
53-
<PopoverContent className="w-72" align="end" sideOffset={4}>
54-
<div className="flex flex-col gap-2 p-1">
55-
<div className="flex flex-col gap-1">
56-
<h4 className="text-xs font-semibold">Premium Feature</h4>
57-
<p className="text-muted-foreground text-xs">
58-
Enable "Remind me later" functionality by upgrading to our paid plans.
59-
</p>
60-
</div>
61-
<div className="flex justify-end">
62-
<LinkButton
63-
size="sm"
64-
variant="primary"
65-
onClick={() => navigate(ROUTES.SETTINGS_BILLING + '?utm_source=enable_snooze_prompt')}
66-
>
67-
Upgrade Plan
68-
</LinkButton>
69-
</div>
70-
</div>
71-
</PopoverContent>
72-
</Popover>
73-
) : (
74-
<Tooltip>
75-
<TooltipTrigger asChild>
76-
<Switch id={id} checked={true} disabled={true} />
77-
</TooltipTrigger>
78-
<TooltipContent>This feature is automatically enabled with your plan and stays active.</TooltipContent>
79-
</Tooltip>
80-
)}
81-
</div>
82-
);
83-
}
84-
8532
function NovuBrandingSwitch({
8633
id,
8734
value,
@@ -139,8 +86,6 @@ export function GeneralSettings({
13986
disabledPrimary,
14087
isForInAppStep,
14188
}: GeneralSettingsProps) {
142-
const isSnoozeEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_SNOOZE_ENABLED);
143-
14489
return (
14590
<div className="border-neutral-alpha-200 bg-background text-foreground-600 mx-0 mt-0 flex flex-col gap-2 rounded-lg border p-3">
14691
<FormField
@@ -183,28 +128,6 @@ export function GeneralSettings({
183128
);
184129
}}
185130
/>
186-
{isSnoozeEnabled && (
187-
<FormField
188-
control={control}
189-
name="enableSnooze"
190-
render={() => {
191-
return (
192-
<FormItem className="flex items-center justify-between gap-2">
193-
<FormLabel
194-
className="text-xs"
195-
htmlFor="enableSnooze"
196-
tooltip="Enables users to postpone notifications and get reminded at a later time"
197-
>
198-
Enable "Remind me later" functionality
199-
</FormLabel>
200-
<FormControl>
201-
<EnableSnoozeSwitch id="enableSnooze" />
202-
</FormControl>
203-
</FormItem>
204-
);
205-
}}
206-
/>
207-
)}
208131
</>
209132
)}
210133

apps/dashboard/src/components/workflow-editor/steps/email/maily.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Editor } from '@maily-to/core';
33
import { Editor as EditorDigest } from '@maily-to/core-digest';
44
import type { Editor as TiptapEditor } from '@tiptap/core';
55
import { Editor as TiptapEditorReact } from '@tiptap/react';
6+
import { FeatureFlagsKeysEnum } from '@novu/shared';
67

78
import { useWorkflow } from '@/components/workflow-editor/workflow-provider';
89
import { useParseVariables } from '@/hooks/use-parse-variables';
@@ -11,8 +12,8 @@ import { cn } from '@/utils/ui';
1112
import { createEditorBlocks, createExtensions, DEFAULT_EDITOR_CONFIG, MAILY_EMAIL_WIDTH } from './maily-config';
1213
import { calculateVariables, VariableFrom } from './variables/variables';
1314
import { RepeatMenuDescription } from './views/repeat-menu-description';
14-
import { FeatureFlagsKeysEnum } from '@novu/shared';
1515
import { useFeatureFlag } from '@/hooks/use-feature-flag';
16+
import { useRemoveGrammarly } from '@/hooks/use-remove-grammarly';
1617

1718
type MailyProps = HTMLAttributes<HTMLDivElement> & {
1819
value: string;
@@ -42,6 +43,7 @@ export const Maily = ({ value, onChange, className, ...rest }: MailyProps) => {
4243
const blocks = useMemo(() => {
4344
return createEditorBlocks({ track, digestStepBeforeCurrent, isEnhancedDigestEnabled });
4445
}, [digestStepBeforeCurrent, isEnhancedDigestEnabled, track]);
46+
const editorParentRef = useRemoveGrammarly<HTMLDivElement>();
4547

4648
const handleCalculateVariables = useCallback(
4749
({ query, editor, from }: { query: string; editor: TiptapEditor; from: VariableFrom }) => {
@@ -125,10 +127,19 @@ export const Maily = ({ value, onChange, className, ...rest }: MailyProps) => {
125127
<>
126128
{overrideTippyBoxStyles()}
127129
<div
130+
ref={editorParentRef}
128131
className={cn(
129132
`shadow-xs mx-auto flex min-h-full max-w-[${MAILY_EMAIL_WIDTH}px] flex-col items-start rounded-lg bg-white [&_a]:pointer-events-none`,
130133
className
131134
)}
135+
data-gramm={false}
136+
data-gramm_editor={false}
137+
data-enable-grammarly="false"
138+
aria-autocomplete="none"
139+
aria-multiline={false}
140+
autoCapitalize="off"
141+
autoCorrect="off"
142+
spellCheck={false}
132143
{...rest}
133144
>
134145
<_Editor

apps/dashboard/src/components/workflow-editor/steps/email/views/for-view.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function ForView(props: NodeViewProps) {
1212
draggable="true"
1313
data-drag-handle=""
1414
data-type="repeat"
15-
className="mly-relative border-soft-100 mx-[-0.5rem] rounded-md border px-2 py-2"
15+
className="mly-relative border-soft-100 mx-[-0.5rem] rounded-md border px-3 py-3"
1616
>
1717
<NodeViewContent className="is-editable" />
1818
<div
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useLayoutEffect, useRef } from 'react';
2+
import { useDataRef } from './use-data-ref';
3+
4+
type MutationObserverOptions = {
5+
childList?: boolean;
6+
attributes?: boolean;
7+
characterData?: boolean;
8+
subtree?: boolean;
9+
attributeOldValue?: boolean;
10+
characterDataOldValue?: boolean;
11+
attributeFilter?: string[];
12+
};
13+
14+
type UseMutationObserverProps = {
15+
target: React.RefObject<Node> | Node | null;
16+
callback: MutationCallback;
17+
options?: MutationObserverOptions;
18+
};
19+
20+
export function useMutationObserver({
21+
target,
22+
callback,
23+
options = { childList: true, subtree: true },
24+
}: UseMutationObserverProps) {
25+
const observerRef = useRef<MutationObserver | null>(null);
26+
const callbackRef = useDataRef<MutationCallback>(callback);
27+
28+
useLayoutEffect(() => {
29+
const targetNode = target && 'current' in target ? target.current : target;
30+
if (!targetNode) return;
31+
32+
// Create MutationObserver with reference to the latest callback
33+
const observer = new MutationObserver((mutations, observer) => {
34+
callbackRef.current(mutations, observer);
35+
});
36+
37+
// Store observer in ref
38+
observerRef.current = observer;
39+
40+
// Start observing
41+
observer.observe(targetNode, options);
42+
43+
return () => {
44+
observer.disconnect();
45+
observerRef.current = null;
46+
};
47+
}, [callbackRef, target, options]);
48+
49+
return observerRef;
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { useRef, useCallback } from 'react';
2+
3+
import { useMutationObserver } from './use-mutation-observer';
4+
5+
export function useRemoveGrammarly<T extends HTMLElement>() {
6+
const target = useRef<T>(null);
7+
const handleGrammarlyRemoval = useCallback((mutations: MutationRecord[]) => {
8+
for (const mutation of mutations) {
9+
if (mutation.type === 'childList') {
10+
for (const node of mutation.addedNodes) {
11+
const isGrammarlyElement =
12+
node instanceof HTMLElement && node.shadowRoot && node.tagName === 'GRAMMARLY-EXTENSION';
13+
14+
if (isGrammarlyElement) {
15+
node.remove();
16+
}
17+
}
18+
}
19+
}
20+
}, []);
21+
22+
useMutationObserver({
23+
target: target,
24+
callback: handleGrammarlyRemoval,
25+
options: { childList: true, subtree: true },
26+
});
27+
28+
return target;
29+
}

libs/dal/src/repositories/message/message.repository.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,11 @@ export class MessageRepository extends BaseRepository<MessageDBModel, MessageEnt
9696
}
9797

9898
if (query.snoozed != null) {
99-
requestQuery.snoozedUntil = { $exists: true, $ne: null };
100-
} else {
101-
requestQuery.snoozedUntil = { $exists: false };
99+
if (query.snoozed) {
100+
requestQuery.snoozedUntil = { $ne: null };
101+
} else {
102+
requestQuery.$or = [{ snoozedUntil: { $exists: false } }, { snoozedUntil: null }];
103+
}
102104
}
103105

104106
if (createdAt != null) {

packages/js/package.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
"build": "pnpm run clean && NODE_ENV=production tsup",
7373
"postbuild": "rm ./src/ui/index.directcss && ./scripts/copy-package-json.sh && node scripts/size-limit.mjs && pnpm run check-exports",
7474
"build:umd": "webpack --config webpack.config.cjs",
75-
"build:watch": "concurrently \"NODE_ENV=development pnpm run tsup:watch\" \"pnpm run start:server\"",
75+
"build:watch": "concurrently \"pnpm run prebuild\" \"NODE_ENV=development pnpm run tsup:watch\" \"pnpm run start:server\"",
7676
"tsup:watch": "tsup --watch",
7777
"check-exports": "attw --pack .",
7878
"lint": "eslint src",
@@ -132,7 +132,7 @@
132132
"mitt": "^3.0.1",
133133
"socket.io-client": "4.7.2",
134134
"solid-floating-ui": "^0.3.1",
135-
"solid-js": "^1.8.11",
135+
"solid-js": "^1.9.4",
136136
"solid-motionone": "^1.0.3",
137137
"tailwind-merge": "^2.4.0"
138138
},

packages/js/src/ui/components/Inbox.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { useInboxContext } from '../context';
44
import { useStyle } from '../helpers';
55
import type {
66
BellRenderer,
7+
BodyRenderer,
78
NotificationActionClickHandler,
89
NotificationClickHandler,
910
NotificationRenderer,
1011
SubjectRenderer,
11-
BodyRenderer,
1212
} from '../types';
1313
import { Bell, Footer, Header, Preferences } from './elements';
1414
import { PreferencesHeader } from './elements/Preferences/PreferencesHeader';

packages/js/src/ui/components/InboxTabs/InboxTabs.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { useTabsDropdown } from '../../helpers/useTabsDropdown';
66
import { Check } from '../../icons';
77
import { ArrowDown } from '../../icons/ArrowDown';
88
import {
9+
BodyRenderer,
910
NotificationActionClickHandler,
1011
NotificationClickHandler,
1112
NotificationRenderer,
1213
NotificationStatus,
1314
SubjectRenderer,
14-
BodyRenderer,
1515
Tab,
1616
} from '../../types';
1717
import { NotificationList } from '../Notification';

0 commit comments

Comments
 (0)