@12-apps/notifications 2.0.0 → 2.1.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/notifications",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Plug-and-play notification system (12-15): an always-on in-app inbox, per-user × per-category channel preferences, and email / SMS / WhatsApp / web-push transports behind vendor DRIVERS so a second provider is a config entry. Framework-free core (.), host-mounted backend surface (./server: inbox / preferences / push-subscription endpoints, the channel router with delivery records + retry sweep, the permission fan-out, duck-typed Prisma seam), Hono adapter (./hono), React surface (./react: bell + badge, inbox drawer, preferences screen), VAPID sender (./web-push) and the package-owned Prisma partial + migrations. Standardized adoption contract in ADOPTING.md.",
|
|
6
6
|
"exports": {
|
|
@@ -11,7 +11,7 @@ import { Box } from '@12-apps/ui/mui/Box';
|
|
|
11
11
|
import type { NotificationMessages } from '../messages';
|
|
12
12
|
|
|
13
13
|
import { BellIcon } from './bell-icon';
|
|
14
|
-
import { useUnreadCount, type NotificationsSubscribe } from './hooks';
|
|
14
|
+
import { useUnreadCount, type NotificationsSignalHook, type NotificationsSubscribe } from './hooks';
|
|
15
15
|
import type { InboxStore } from './inbox-state';
|
|
16
16
|
|
|
17
17
|
const triggerSx = {
|
|
@@ -46,14 +46,17 @@ export function BellButton({
|
|
|
46
46
|
store,
|
|
47
47
|
messages,
|
|
48
48
|
subscribe,
|
|
49
|
+
useSignal,
|
|
49
50
|
}: BellButtonProps & {
|
|
50
51
|
store: InboxStore;
|
|
51
52
|
messages: NotificationMessages;
|
|
52
53
|
subscribe?: NotificationsSubscribe;
|
|
54
|
+
useSignal?: NotificationsSignalHook;
|
|
53
55
|
}): JSX.Element {
|
|
54
56
|
const count = useUnreadCount(store, {
|
|
55
57
|
enabled,
|
|
56
58
|
...(subscribe ? { subscribe } : {}),
|
|
59
|
+
...(useSignal ? { useSignal } : {}),
|
|
57
60
|
});
|
|
58
61
|
return (
|
|
59
62
|
<Box
|
|
@@ -4,7 +4,11 @@ import { messagesOf, type NotificationMessages } from '../messages';
|
|
|
4
4
|
|
|
5
5
|
import { createNotificationsApiClient, type NotificationsApiClient } from './api';
|
|
6
6
|
import { BellButton, type BellButtonProps } from './bell-button';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
useUnreadCount,
|
|
9
|
+
type NotificationsSignalHook,
|
|
10
|
+
type NotificationsSubscribe,
|
|
11
|
+
} from './hooks';
|
|
8
12
|
import { createInboxStore, type InboxStore } from './inbox-state';
|
|
9
13
|
import { NotificationsPanel, type NotificationsPanelProps } from './panel';
|
|
10
14
|
import { PreferencesScreen, type PreferencesScreenProps } from './preferences-screen';
|
|
@@ -41,6 +45,12 @@ export interface NotificationsWebConfig {
|
|
|
41
45
|
* correctness.
|
|
42
46
|
*/
|
|
43
47
|
subscribe?: NotificationsSubscribe;
|
|
48
|
+
/**
|
|
49
|
+
* The same wiring as a HOOK, for a host whose realtime connection lives in
|
|
50
|
+
* React context — see `NotificationsSignalHook`. `subscribe` is read at
|
|
51
|
+
* factory time, which such a host cannot reach.
|
|
52
|
+
*/
|
|
53
|
+
useSignal?: NotificationsSignalHook;
|
|
44
54
|
/** The browser push enable step's host seams (SW path, platform hint). */
|
|
45
55
|
webPush?: WebPushSetupConfig;
|
|
46
56
|
}
|
|
@@ -79,7 +89,10 @@ export function createWebNotifications(config: NotificationsWebConfig): WebNotif
|
|
|
79
89
|
const store = createInboxStore(api);
|
|
80
90
|
const webPush = config.webPush ?? {};
|
|
81
91
|
const subscribe = config.subscribe;
|
|
82
|
-
const subscribeOption =
|
|
92
|
+
const subscribeOption = {
|
|
93
|
+
...(subscribe ? { subscribe } : {}),
|
|
94
|
+
...(config.useSignal ? { useSignal: config.useSignal } : {}),
|
|
95
|
+
};
|
|
83
96
|
|
|
84
97
|
const Bell: ComponentType<BellButtonProps> = (props) => (
|
|
85
98
|
<BellButton {...props} store={store} messages={messages} {...subscribeOption} />
|
package/src/react/hooks.ts
CHANGED
|
@@ -24,6 +24,26 @@ import {
|
|
|
24
24
|
*/
|
|
25
25
|
export type NotificationsSubscribe = (onHint: () => void) => () => void;
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* The same wiring, as a HOOK — for a host whose realtime connection lives in
|
|
29
|
+
* React context rather than in a module.
|
|
30
|
+
*
|
|
31
|
+
* `subscribe` above is supplied at FACTORY time, which is module scope, and a
|
|
32
|
+
* context-bound connection cannot be reached from there: the provider holding
|
|
33
|
+
* it is inside the tree. A host in that shape (a `<UserRealtimeProvider>` and a
|
|
34
|
+
* `useUserTopics` hook, which is the common one) had no way to pass anything at
|
|
35
|
+
* all, and the badge simply never heard an event.
|
|
36
|
+
*
|
|
37
|
+
* So this is the second door, and it is the one `@12-apps/app-shell` already
|
|
38
|
+
* uses for the same problem — its consent dialog takes a `useSignal` hook for
|
|
39
|
+
* exactly this reason. Two packages solving one problem two ways is how an
|
|
40
|
+
* adopter ends up believing the feature is unavailable to it.
|
|
41
|
+
*
|
|
42
|
+
* Called during render, so it may use context and hooks freely. Pass one or
|
|
43
|
+
* the other; passing both runs both, which is a host's business.
|
|
44
|
+
*/
|
|
45
|
+
export type NotificationsSignalHook = (onHint: () => void) => void;
|
|
46
|
+
|
|
27
47
|
export function useInboxState(store: InboxStore): InboxState {
|
|
28
48
|
return useSyncExternalStore(store.subscribe, store.getState, store.getState);
|
|
29
49
|
}
|
|
@@ -36,12 +56,22 @@ export function useInboxState(store: InboxStore): InboxState {
|
|
|
36
56
|
*/
|
|
37
57
|
export function useUnreadCount(
|
|
38
58
|
store: InboxStore,
|
|
39
|
-
options: {
|
|
59
|
+
options: {
|
|
60
|
+
enabled?: boolean;
|
|
61
|
+
subscribe?: NotificationsSubscribe;
|
|
62
|
+
useSignal?: NotificationsSignalHook;
|
|
63
|
+
} = {},
|
|
40
64
|
): number {
|
|
41
65
|
const enabled = options.enabled ?? true;
|
|
42
66
|
const subscribe = options.subscribe;
|
|
43
67
|
const { unread } = useInboxState(store);
|
|
44
68
|
|
|
69
|
+
// Called unconditionally — it is a hook, so it cannot sit behind `enabled`.
|
|
70
|
+
// The host's own hook decides what to do when there is nothing to hear.
|
|
71
|
+
options.useSignal?.(() => {
|
|
72
|
+
if (enabled) store.invalidate();
|
|
73
|
+
});
|
|
74
|
+
|
|
45
75
|
useEffect(() => {
|
|
46
76
|
if (!enabled) return;
|
|
47
77
|
store.refreshBadge();
|