@pushwoosh/smart-components-kit 1.3.17 → 1.3.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ProductUpdates/ProductUpdates.js +10 -4
- package/ProductUpdates/constants.d.ts +1 -1
- package/ProductUpdates/constants.js +3 -1
- package/ProductUpdates/helpers.js +9 -3
- package/ProductUpdates/hooks/useProductUpdates.d.ts +3 -0
- package/ProductUpdates/hooks/useProductUpdates.js +16 -9
- package/package.json +1 -1
|
@@ -18,7 +18,8 @@ export function ProductUpdates({
|
|
|
18
18
|
unreadCount,
|
|
19
19
|
isLoading,
|
|
20
20
|
hasUpdates,
|
|
21
|
-
markAllRead
|
|
21
|
+
markAllRead,
|
|
22
|
+
clearNewBadges
|
|
22
23
|
} = useProductUpdates();
|
|
23
24
|
useEffect(() => {
|
|
24
25
|
if (!isAllowed || !autoOpenOnUnread || hasAutoOpened.current || unreadCount === 0) {
|
|
@@ -26,10 +27,15 @@ export function ProductUpdates({
|
|
|
26
27
|
}
|
|
27
28
|
hasAutoOpened.current = true;
|
|
28
29
|
setIsOpen(true);
|
|
29
|
-
|
|
30
|
+
markAllRead();
|
|
31
|
+
}, [isAllowed, autoOpenOnUnread, unreadCount, markAllRead]);
|
|
32
|
+
const handleOpen = () => {
|
|
33
|
+
setIsOpen(true);
|
|
34
|
+
markAllRead();
|
|
35
|
+
};
|
|
30
36
|
const handleClose = () => {
|
|
31
37
|
setIsOpen(false);
|
|
32
|
-
|
|
38
|
+
clearNewBadges();
|
|
33
39
|
};
|
|
34
40
|
if (!isAllowed || isLoading || !hasUpdates) {
|
|
35
41
|
return null;
|
|
@@ -43,7 +49,7 @@ export function ProductUpdates({
|
|
|
43
49
|
size: "medium",
|
|
44
50
|
view: "lined"
|
|
45
51
|
}),
|
|
46
|
-
onClick:
|
|
52
|
+
onClick: handleOpen,
|
|
47
53
|
children: lang({
|
|
48
54
|
message: 'SMART.PRODUCT_UPDATES.BUTTON'
|
|
49
55
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AlertStatus, AlertType, OrderBy, OrderDirection } from '@pushwoosh/rpc-v2-http-api-data/pushwoosh_accounts_alerts_v1';
|
|
2
|
-
export declare const
|
|
2
|
+
export declare const MAX_VISIBLE_UPDATES = 10;
|
|
3
3
|
export declare const ALERT_TYPE_PRODUCT_UPDATE: AlertType;
|
|
4
4
|
export declare const ALERT_STATUS_READ: AlertStatus;
|
|
5
5
|
export declare const ALERT_STATUS_UNREAD: AlertStatus;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
// Product updates carry no expiry, so the feed only grows. The modal shows the newest
|
|
2
|
+
// slice and never paginates into the past: older ones stay in the notification history.
|
|
3
|
+
export const MAX_VISIBLE_UPDATES = 10;
|
|
2
4
|
export const ALERT_TYPE_PRODUCT_UPDATE = 'ALERT_TYPE_PRODUCT_UPDATE';
|
|
3
5
|
export const ALERT_STATUS_READ = 'ALERT_STATUS_READ';
|
|
4
6
|
export const ALERT_STATUS_UNREAD = 'ALERT_STATUS_UNREAD';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// The root config resolves the app-code placeholder only on a real page load; the same
|
|
2
|
+
// push inside the SPA answers 404 for the literal "app-code" and lands on /projects.
|
|
3
|
+
const APP_CODE_PLACEHOLDER = /^\/(applications|journeys)\/app-code(?=\/|$)/i;
|
|
3
4
|
export function openActionLink(link, history) {
|
|
4
5
|
let url;
|
|
5
6
|
try {
|
|
@@ -11,7 +12,12 @@ export function openActionLink(link, history) {
|
|
|
11
12
|
window.open(url.href, '_blank', 'noopener,noreferrer');
|
|
12
13
|
return false;
|
|
13
14
|
}
|
|
14
|
-
|
|
15
|
+
const path = `${url.pathname}${url.search}${url.hash}`;
|
|
16
|
+
if (APP_CODE_PLACEHOLDER.test(url.pathname)) {
|
|
17
|
+
window.location.assign(path);
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
history.push(path);
|
|
15
21
|
return true;
|
|
16
22
|
}
|
|
17
23
|
// UTC on purpose: an announcement is dated once for everyone, and a local-time
|
|
@@ -4,7 +4,10 @@ type UseProductUpdates = {
|
|
|
4
4
|
readonly unreadCount: number;
|
|
5
5
|
readonly isLoading: boolean;
|
|
6
6
|
readonly hasUpdates: boolean;
|
|
7
|
+
/** Sends the read marks. Call on open: showing the modal is the delivery. */
|
|
7
8
|
readonly markAllRead: () => Promise<void>;
|
|
9
|
+
/** Drops the NEW badges and the unread counter. Call on close, so they last while reading. */
|
|
10
|
+
readonly clearNewBadges: () => void;
|
|
8
11
|
};
|
|
9
12
|
export declare function useProductUpdates(): UseProductUpdates;
|
|
10
13
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useCallback, useState } from 'react';
|
|
1
|
+
import { useCallback, useRef, useState } from 'react';
|
|
2
2
|
import { useAsync } from '@pushwoosh/kit-helpers';
|
|
3
3
|
import { useSPAContext } from '@pushwoosh/spa-context';
|
|
4
|
-
import { ALERT_STATUS_READ, ALERT_STATUS_UNREAD, ALERT_TYPE_PRODUCT_UPDATE, ORDER_BY_CREATED, ORDER_DIRECTION_DESC,
|
|
4
|
+
import { ALERT_STATUS_READ, ALERT_STATUS_UNREAD, ALERT_TYPE_PRODUCT_UPDATE, ORDER_BY_CREATED, ORDER_DIRECTION_DESC, MAX_VISIBLE_UPDATES } from '../constants';
|
|
5
5
|
import { useIsAllowProductUpdates } from './useIsAllowProductUpdates';
|
|
6
6
|
const EMPTY = [];
|
|
7
7
|
export function useProductUpdates() {
|
|
@@ -9,7 +9,8 @@ export function useProductUpdates() {
|
|
|
9
9
|
rpcGateway
|
|
10
10
|
} = useSPAContext();
|
|
11
11
|
const isAllowed = useIsAllowProductUpdates();
|
|
12
|
-
const [
|
|
12
|
+
const [areBadgesCleared, setAreBadgesCleared] = useState(false);
|
|
13
|
+
const hasSentRef = useRef(false);
|
|
13
14
|
const state = useAsync(async () => {
|
|
14
15
|
if (!isAllowed) {
|
|
15
16
|
return EMPTY;
|
|
@@ -17,24 +18,28 @@ export function useProductUpdates() {
|
|
|
17
18
|
const response = await rpcGateway.internalAlerts.List({
|
|
18
19
|
type: ALERT_TYPE_PRODUCT_UPDATE,
|
|
19
20
|
page: 0,
|
|
20
|
-
perPage:
|
|
21
|
+
perPage: MAX_VISIBLE_UPDATES,
|
|
21
22
|
orderBy: ORDER_BY_CREATED,
|
|
22
23
|
orderDirection: ORDER_DIRECTION_DESC
|
|
23
24
|
});
|
|
24
25
|
return response.alerts;
|
|
25
26
|
}, [rpcGateway, isAllowed]);
|
|
26
27
|
const fetched = state.value ?? EMPTY;
|
|
27
|
-
|
|
28
|
-
// so one flag is enough to reflect it without refetching.
|
|
29
|
-
const updates = isMarkedRead ? fetched.map(update => ({
|
|
28
|
+
const updates = areBadgesCleared ? fetched.map(update => ({
|
|
30
29
|
...update,
|
|
31
30
|
status: ALERT_STATUS_READ
|
|
32
31
|
})) : fetched;
|
|
32
|
+
// The mark goes out on open, not on close: a reload with the modal still open is not
|
|
33
|
+
// a close, and the same updates used to pop again.
|
|
33
34
|
const markAllRead = useCallback(async () => {
|
|
35
|
+
if (hasSentRef.current) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
34
38
|
const unread = fetched.filter(update => update.status === ALERT_STATUS_UNREAD);
|
|
35
39
|
if (unread.length === 0) {
|
|
36
40
|
return;
|
|
37
41
|
}
|
|
42
|
+
hasSentRef.current = true;
|
|
38
43
|
try {
|
|
39
44
|
await rpcGateway.internalAlerts.UpdateStatus({
|
|
40
45
|
alerts: unread.map(update => ({
|
|
@@ -42,16 +47,18 @@ export function useProductUpdates() {
|
|
|
42
47
|
status: ALERT_STATUS_READ
|
|
43
48
|
}))
|
|
44
49
|
});
|
|
45
|
-
setIsMarkedRead(true);
|
|
46
50
|
} catch (error) {
|
|
47
51
|
console.error('Failed to mark product updates as read:', error);
|
|
52
|
+
hasSentRef.current = false;
|
|
48
53
|
}
|
|
49
54
|
}, [rpcGateway, fetched]);
|
|
55
|
+
const clearNewBadges = useCallback(() => setAreBadgesCleared(true), []);
|
|
50
56
|
return {
|
|
51
57
|
updates,
|
|
52
58
|
unreadCount: updates.filter(update => update.status === ALERT_STATUS_UNREAD).length,
|
|
53
59
|
isLoading: state.loading,
|
|
54
60
|
hasUpdates: updates.length > 0,
|
|
55
|
-
markAllRead
|
|
61
|
+
markAllRead,
|
|
62
|
+
clearNewBadges
|
|
56
63
|
};
|
|
57
64
|
}
|
package/package.json
CHANGED