@streamlayer/sdk-web-notifications 0.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/README.md +6 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +11 -0
- package/lib/notifications.d.ts +57 -0
- package/lib/notifications.js +35 -0
- package/lib/queue/index.d.ts +22 -0
- package/lib/queue/index.js +124 -0
- package/lib/storage.d.ts +7 -0
- package/lib/storage.js +19 -0
- package/package.json +31 -0
package/README.md
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import { Notifications } from './notifications';
|
|
3
|
+
export { type Notification, type NotificationData, NotificationType, Notifications } from './notifications';
|
|
4
|
+
export { type NotificationsList } from './queue';
|
|
5
|
+
export type NotificationsStore = ReturnType<Notifications['getQueueStore']>;
|
|
6
|
+
declare module '@streamlayer/sdk-web-interfaces' {
|
|
7
|
+
interface StreamLayerContext {
|
|
8
|
+
notifications: Notifications;
|
|
9
|
+
addNotification: Notifications['add'];
|
|
10
|
+
}
|
|
11
|
+
interface StreamLayerSDK {
|
|
12
|
+
getNotificationsStore: Notifications['getQueueStore'];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* notifications plugin, connect notifications to sdk
|
|
17
|
+
*/
|
|
18
|
+
export declare const notifications: (instance: StreamLayerContext, opts: unknown, done: () => void) => void;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Notifications } from './notifications';
|
|
2
|
+
export { NotificationType, Notifications } from './notifications';
|
|
3
|
+
/**
|
|
4
|
+
* notifications plugin, connect notifications to sdk
|
|
5
|
+
*/
|
|
6
|
+
export const notifications = (instance, opts, done) => {
|
|
7
|
+
instance.notifications = new Notifications();
|
|
8
|
+
instance.addNotification = instance.notifications.add;
|
|
9
|
+
instance.sdk.getNotificationsStore = () => instance.notifications.getQueueStore();
|
|
10
|
+
done();
|
|
11
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { InstantView, QuestionType, GameSettings } from '@streamlayer/sdk-web-types';
|
|
2
|
+
import { NotificationsQueue, NotificationsQueueOptions } from './queue';
|
|
3
|
+
export type NotificationData = {
|
|
4
|
+
questionType: QuestionType;
|
|
5
|
+
question?: {
|
|
6
|
+
title: string;
|
|
7
|
+
votedAnswer?: {
|
|
8
|
+
title?: string;
|
|
9
|
+
points?: number;
|
|
10
|
+
};
|
|
11
|
+
predictionResult?: boolean;
|
|
12
|
+
correct?: boolean;
|
|
13
|
+
correctAnswerTitle?: string;
|
|
14
|
+
questionTitle?: string;
|
|
15
|
+
};
|
|
16
|
+
insight?: InstantView;
|
|
17
|
+
onboarding?: GameSettings;
|
|
18
|
+
tweet?: {
|
|
19
|
+
title: string;
|
|
20
|
+
image: string;
|
|
21
|
+
body: string;
|
|
22
|
+
account: {
|
|
23
|
+
image: string;
|
|
24
|
+
name: string;
|
|
25
|
+
userName: string;
|
|
26
|
+
verified: boolean;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export declare enum NotificationType {
|
|
31
|
+
ONBOARDING = 1,
|
|
32
|
+
QUESTION = 2,
|
|
33
|
+
QUESTION_RESOLVED = 3
|
|
34
|
+
}
|
|
35
|
+
export type Notification<M extends Record<string, Function> = never> = {
|
|
36
|
+
autoHideDuration: number;
|
|
37
|
+
delay?: number;
|
|
38
|
+
hiding?: boolean;
|
|
39
|
+
type: NotificationType;
|
|
40
|
+
action?: (...args: unknown[]) => void;
|
|
41
|
+
close?: (...args: unknown[]) => void;
|
|
42
|
+
methods?: M;
|
|
43
|
+
data: NotificationData;
|
|
44
|
+
id: string;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* @description app notifications (inapp)
|
|
48
|
+
*/
|
|
49
|
+
export declare class Notifications {
|
|
50
|
+
queue: NotificationsQueue;
|
|
51
|
+
private storage;
|
|
52
|
+
constructor(options?: Partial<NotificationsQueueOptions>);
|
|
53
|
+
add: (notification: Notification) => void;
|
|
54
|
+
close: (notificationId: string) => void;
|
|
55
|
+
getQueueStore: () => import("nanostores").WritableAtom<Map<string, Notification> | undefined>;
|
|
56
|
+
markAsViewed: (notificationId: string) => void;
|
|
57
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NotificationsQueue } from './queue';
|
|
2
|
+
import { NotificationStorage } from './storage';
|
|
3
|
+
export var NotificationType;
|
|
4
|
+
(function (NotificationType) {
|
|
5
|
+
NotificationType[NotificationType["ONBOARDING"] = 1] = "ONBOARDING";
|
|
6
|
+
NotificationType[NotificationType["QUESTION"] = 2] = "QUESTION";
|
|
7
|
+
NotificationType[NotificationType["QUESTION_RESOLVED"] = 3] = "QUESTION_RESOLVED";
|
|
8
|
+
})(NotificationType || (NotificationType = {}));
|
|
9
|
+
/**
|
|
10
|
+
* @description app notifications (inapp)
|
|
11
|
+
*/
|
|
12
|
+
export class Notifications {
|
|
13
|
+
queue;
|
|
14
|
+
storage;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.storage = new NotificationStorage();
|
|
17
|
+
this.queue = new NotificationsQueue({ concurrency: 1, animationDelay: 1600, ...options });
|
|
18
|
+
}
|
|
19
|
+
add = (notification) => {
|
|
20
|
+
const isViewed = this.storage.isOpened(notification.id);
|
|
21
|
+
if (!isViewed) {
|
|
22
|
+
this.queue.addToQueue(notification);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
close = (notificationId) => {
|
|
26
|
+
this.queue.closeNotification(notificationId);
|
|
27
|
+
this.markAsViewed(notificationId);
|
|
28
|
+
};
|
|
29
|
+
getQueueStore = () => {
|
|
30
|
+
return this.queue.notificationsList;
|
|
31
|
+
};
|
|
32
|
+
markAsViewed = (notificationId) => {
|
|
33
|
+
this.storage.setOpened(notificationId);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SingleStore, createComputedStore } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import { Notification } from '..';
|
|
3
|
+
export type NotificationsQueueOptions = {
|
|
4
|
+
concurrency: number;
|
|
5
|
+
animationDelay: number;
|
|
6
|
+
};
|
|
7
|
+
export type NotificationsList = ReturnType<typeof createComputedStore<Notification[]>>;
|
|
8
|
+
export declare class NotificationsQueue {
|
|
9
|
+
notificationsList: ReturnType<SingleStore<Map<Notification['id'], Notification>>['getStore']>;
|
|
10
|
+
private notifications;
|
|
11
|
+
private store;
|
|
12
|
+
private timeouts;
|
|
13
|
+
private waitingQueue;
|
|
14
|
+
private activeQueue;
|
|
15
|
+
private options;
|
|
16
|
+
private logger;
|
|
17
|
+
constructor(options: NotificationsQueueOptions);
|
|
18
|
+
addToQueue: (notification: Notification) => void;
|
|
19
|
+
tickWaitingQueue: () => void;
|
|
20
|
+
tickActiveQueue: (notificationId: string) => void;
|
|
21
|
+
closeNotification: (notificationId: string) => void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { SingleStore, createSingleStore } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
3
|
+
export class NotificationsQueue {
|
|
4
|
+
notificationsList;
|
|
5
|
+
notifications;
|
|
6
|
+
store;
|
|
7
|
+
timeouts;
|
|
8
|
+
waitingQueue;
|
|
9
|
+
activeQueue;
|
|
10
|
+
options;
|
|
11
|
+
logger;
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.logger = createLogger('notifications');
|
|
15
|
+
this.store = new Map();
|
|
16
|
+
this.timeouts = new Map();
|
|
17
|
+
this.waitingQueue = new Set();
|
|
18
|
+
this.activeQueue = new Set();
|
|
19
|
+
this.notifications = new SingleStore(createSingleStore(new Map()), 'notifications-queue');
|
|
20
|
+
this.notificationsList = this.notifications.getStore();
|
|
21
|
+
}
|
|
22
|
+
addToQueue = (notification) => {
|
|
23
|
+
if (this.store.has(notification.id)) {
|
|
24
|
+
this.logger.debug({ notification }, 'skip existed notification: %o');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const close = notification.close;
|
|
28
|
+
const action = notification.action;
|
|
29
|
+
this.store.set(notification.id, {
|
|
30
|
+
...notification,
|
|
31
|
+
close: (...args) => {
|
|
32
|
+
if (close) {
|
|
33
|
+
close(...args);
|
|
34
|
+
}
|
|
35
|
+
this.closeNotification(notification.id);
|
|
36
|
+
},
|
|
37
|
+
action: (...args) => {
|
|
38
|
+
if (action) {
|
|
39
|
+
action(...args);
|
|
40
|
+
}
|
|
41
|
+
this.closeNotification(notification.id);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
this.waitingQueue.add(notification.id);
|
|
45
|
+
/**
|
|
46
|
+
* Hide an oldest notification if the active queue is full,
|
|
47
|
+
* and show a new notification on the next tick
|
|
48
|
+
*/
|
|
49
|
+
if (this.activeQueue.size === this.options.concurrency) {
|
|
50
|
+
const [job] = this.activeQueue;
|
|
51
|
+
this.closeNotification(job);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
this.tickWaitingQueue();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
tickWaitingQueue = () => {
|
|
58
|
+
if (this.activeQueue.size < this.options.concurrency) {
|
|
59
|
+
const [job] = this.waitingQueue;
|
|
60
|
+
if (!job) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
this.activeQueue.add(job);
|
|
64
|
+
this.waitingQueue.delete(job);
|
|
65
|
+
this.logger.debug({ job }, 'waiting queue tick');
|
|
66
|
+
this.tickActiveQueue(job);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
this.logger.debug({ queueSize: this.activeQueue.size, concurrency: this.options.concurrency }, 'waiting queue tick skipped');
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
tickActiveQueue = (notificationId) => {
|
|
73
|
+
if (!notificationId) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const notification = this.store.get(notificationId);
|
|
77
|
+
if (!notification) {
|
|
78
|
+
this.logger.debug({ notificationId }, 'active queue tick skipped, notification not exist');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const timeout = setTimeout(() => {
|
|
82
|
+
const closureId = notificationId;
|
|
83
|
+
const prevQueue = new Map(this.notifications.getValue());
|
|
84
|
+
prevQueue.set(notification.id, notification);
|
|
85
|
+
this.notifications.setValue(prevQueue);
|
|
86
|
+
const timeout = setTimeout(() => {
|
|
87
|
+
this.logger.debug({ notificationId: closureId, delay: notification.autoHideDuration || 5000 }, 'notification hiding by autoHideDuration');
|
|
88
|
+
this.closeNotification(closureId);
|
|
89
|
+
}, notification.autoHideDuration || 5000);
|
|
90
|
+
this.timeouts.set(closureId, timeout);
|
|
91
|
+
this.logger.debug({ notificationId: closureId, queue: [...prevQueue.values()] }, 'notification displayed');
|
|
92
|
+
}, notification.delay || 0);
|
|
93
|
+
this.timeouts.set(notificationId, timeout);
|
|
94
|
+
this.logger.debug({ notificationId }, 'active queue tick completed');
|
|
95
|
+
this.tickWaitingQueue();
|
|
96
|
+
};
|
|
97
|
+
closeNotification = (notificationId) => {
|
|
98
|
+
const prevQueue = new Map(this.notifications.getValue());
|
|
99
|
+
const notification = prevQueue.get(notificationId);
|
|
100
|
+
if (notification) {
|
|
101
|
+
// do not hide notification if we have more than one notification in waiting queue,
|
|
102
|
+
// because we need to show next notification immediately
|
|
103
|
+
notification.hiding = !(this.waitingQueue.size >= this.options.concurrency);
|
|
104
|
+
this.notifications.setValue(prevQueue);
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
const prevQueue = new Map(this.notifications.getValue());
|
|
107
|
+
prevQueue.delete(notificationId);
|
|
108
|
+
this.notifications.setValue(prevQueue);
|
|
109
|
+
const timeout = this.timeouts.get(notificationId);
|
|
110
|
+
if (timeout !== undefined) {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
this.timeouts.delete(notificationId);
|
|
113
|
+
}
|
|
114
|
+
this.logger.debug({ notificationId }, 'notification hidden');
|
|
115
|
+
}, this.options.animationDelay || 0);
|
|
116
|
+
this.timeouts.set(notificationId, timeout);
|
|
117
|
+
}
|
|
118
|
+
this.store.delete(notificationId);
|
|
119
|
+
this.activeQueue.delete(notificationId);
|
|
120
|
+
this.waitingQueue.delete(notificationId);
|
|
121
|
+
this.tickWaitingQueue();
|
|
122
|
+
this.logger.debug({ notificationId }, 'notification hiding');
|
|
123
|
+
};
|
|
124
|
+
}
|
package/lib/storage.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Storage } from '@streamlayer/sdk-web-storage';
|
|
2
|
+
export declare class NotificationStorage extends Storage {
|
|
3
|
+
constructor();
|
|
4
|
+
setOpened: (notificationId: string) => void;
|
|
5
|
+
isOpened: (notificationId: string) => string | undefined;
|
|
6
|
+
clearNotification: () => void;
|
|
7
|
+
}
|
package/lib/storage.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Storage } from '@streamlayer/sdk-web-storage';
|
|
2
|
+
var KEY_PREFIX;
|
|
3
|
+
(function (KEY_PREFIX) {
|
|
4
|
+
KEY_PREFIX["OPENED"] = "opened";
|
|
5
|
+
})(KEY_PREFIX || (KEY_PREFIX = {}));
|
|
6
|
+
export class NotificationStorage extends Storage {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('notification');
|
|
9
|
+
}
|
|
10
|
+
setOpened = (notificationId) => {
|
|
11
|
+
this.write(KEY_PREFIX.OPENED, notificationId, 'true');
|
|
12
|
+
};
|
|
13
|
+
isOpened = (notificationId) => {
|
|
14
|
+
return this.read(KEY_PREFIX.OPENED, notificationId);
|
|
15
|
+
};
|
|
16
|
+
clearNotification = () => {
|
|
17
|
+
this.clear();
|
|
18
|
+
};
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@streamlayer/sdk-web-notifications",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./lib/index.js",
|
|
6
|
+
"typings": "./lib/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"lib/",
|
|
9
|
+
"package.json"
|
|
10
|
+
],
|
|
11
|
+
"peerDependencies": {
|
|
12
|
+
"@streamlayer/sdk-web-interfaces": "^0.0.1",
|
|
13
|
+
"@streamlayer/sdk-web-types": "^0.0.1",
|
|
14
|
+
"@streamlayer/sdk-web-logger": "^0.0.1",
|
|
15
|
+
"@streamlayer/sdk-web-storage": "^0.0.4"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"module": "./lib/index.js",
|
|
20
|
+
"require": "./lib/index.js",
|
|
21
|
+
"types": "./lib/index.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"tslib": "^2.6.2",
|
|
26
|
+
"@nx/playwright": "17.1.1",
|
|
27
|
+
"@playwright/test": "^1.39.0",
|
|
28
|
+
"@nx/webpack": "17.0.3",
|
|
29
|
+
"webpack": "^5.89.0"
|
|
30
|
+
}
|
|
31
|
+
}
|