@streamlayer/feature-gamification 0.41.3 → 1.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/lib/advertisement/index.d.ts +38 -0
- package/lib/advertisement/index.js +119 -0
- package/lib/advertisement/queue.d.ts +24 -0
- package/lib/advertisement/queue.js +133 -0
- package/lib/advertisement/storage.d.ts +6 -0
- package/lib/advertisement/storage.js +16 -0
- package/lib/background.d.ts +3 -1
- package/lib/background.js +3 -0
- package/lib/deepLink.js +14 -5
- package/lib/detail.d.ts +3 -3
- package/lib/friendSummary.d.ts +1 -1
- package/lib/gamification.d.ts +2 -1
- package/lib/gamification.js +7 -0
- package/lib/index.d.ts +1 -0
- package/lib/leaderboard.d.ts +1 -1
- package/lib/queries/deepLink.js +9 -4
- package/lib/queries/index.d.ts +4 -3
- package/lib/queries/index.js +14 -0
- package/lib/userSummary.d.ts +1 -1
- package/package.json +13 -13
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createMapStore } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import { PromotionOptions } from '@streamlayer/sdk-web-types';
|
|
3
|
+
import type { Transport } from '@streamlayer/sdk-web-api';
|
|
4
|
+
import { type GamificationBackground } from '../background';
|
|
5
|
+
type AdvertisementData = {
|
|
6
|
+
loading?: boolean;
|
|
7
|
+
error?: unknown;
|
|
8
|
+
data?: PromotionOptions;
|
|
9
|
+
};
|
|
10
|
+
export type Advertisement = {
|
|
11
|
+
autoHideDuration: number;
|
|
12
|
+
delay?: number;
|
|
13
|
+
hiding?: boolean;
|
|
14
|
+
action?: (...args: unknown[]) => void;
|
|
15
|
+
close?: (...args: unknown[]) => void;
|
|
16
|
+
data: ReturnType<typeof createMapStore<AdvertisementData>>;
|
|
17
|
+
promise: () => Promise<void>;
|
|
18
|
+
id: string;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* @name Advertisement
|
|
22
|
+
* @description advertisement functionality, show, hide
|
|
23
|
+
* - show: show advertisement by id, if it was not showed before
|
|
24
|
+
* - hide: hide advertisement by id
|
|
25
|
+
* - markAsViewed: mark advertisement as viewed, called when UI is requested to show advertisement
|
|
26
|
+
* - getActiveAdvertisement: request active advertisement from queue by UI
|
|
27
|
+
*
|
|
28
|
+
* On mount:
|
|
29
|
+
* - we subscribe to $feedList, and show last advertisement from list
|
|
30
|
+
* - we subscribe to $feedSubscription, and show advertisement on activate
|
|
31
|
+
*/
|
|
32
|
+
export declare const advertisement: ($feedList: GamificationBackground["feedList"], $feedSubscription: GamificationBackground["feedSubscription"], transport: Transport) => {
|
|
33
|
+
hide: (notificationId: string) => void;
|
|
34
|
+
show: (advertisementId: string) => void;
|
|
35
|
+
$list: import("nanostores").WritableAtom<Map<string, Advertisement> | undefined>;
|
|
36
|
+
getActiveAdvertisement: () => Advertisement | null;
|
|
37
|
+
};
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createComputedStore, createMapStore, eventBus } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import { QuestionStatus, QuestionType } from '@streamlayer/sdk-web-types';
|
|
3
|
+
import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
4
|
+
import { onMount } from 'nanostores';
|
|
5
|
+
import { getPromotionDetail } from '../queries';
|
|
6
|
+
import { AdvertisementStorage } from './storage';
|
|
7
|
+
import { AdvertisementsQueue } from './queue';
|
|
8
|
+
/**
|
|
9
|
+
* @name Advertisement
|
|
10
|
+
* @description advertisement functionality, show, hide
|
|
11
|
+
* - show: show advertisement by id, if it was not showed before
|
|
12
|
+
* - hide: hide advertisement by id
|
|
13
|
+
* - markAsViewed: mark advertisement as viewed, called when UI is requested to show advertisement
|
|
14
|
+
* - getActiveAdvertisement: request active advertisement from queue by UI
|
|
15
|
+
*
|
|
16
|
+
* On mount:
|
|
17
|
+
* - we subscribe to $feedList, and show last advertisement from list
|
|
18
|
+
* - we subscribe to $feedSubscription, and show advertisement on activate
|
|
19
|
+
*/
|
|
20
|
+
export const advertisement = ($feedList, $feedSubscription, transport) => {
|
|
21
|
+
const logger = createLogger('advertisement_queue');
|
|
22
|
+
const queue = new AdvertisementsQueue({ concurrency: 1, animationDelay: 0 });
|
|
23
|
+
const storage = new AdvertisementStorage();
|
|
24
|
+
const $advertisementList = createComputedStore($feedList.getStore(), (feedList) => {
|
|
25
|
+
return feedList.data?.filter((item) => item.type === 'promotion');
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Show advertisement by id, if it was not showed before.
|
|
29
|
+
*/
|
|
30
|
+
const show = (advertisementId) => {
|
|
31
|
+
if (storage.isShowed(advertisementId)) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
queue.addToQueue({
|
|
35
|
+
id: advertisementId,
|
|
36
|
+
autoHideDuration: Infinity,
|
|
37
|
+
data: createMapStore({ loading: false, error: undefined, data: undefined }),
|
|
38
|
+
promise: async function () {
|
|
39
|
+
this.data.setKey('loading', true);
|
|
40
|
+
try {
|
|
41
|
+
const response = await getPromotionDetail(advertisementId, transport);
|
|
42
|
+
if (!response) {
|
|
43
|
+
this.data.setKey('error', new Error('No promotion found'));
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
this.data.setKey('data', response);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
this.data.setKey('error', error);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
this.data.setKey('loading', false);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const markAsViewed = (notificationId) => {
|
|
59
|
+
logger.debug({ notificationId }, 'markAsViewed: %o');
|
|
60
|
+
storage.setShowed(notificationId);
|
|
61
|
+
};
|
|
62
|
+
const hide = (notificationId) => {
|
|
63
|
+
queue.closeAdvertisement(notificationId);
|
|
64
|
+
markAsViewed(notificationId);
|
|
65
|
+
};
|
|
66
|
+
const getActiveAdvertisement = () => {
|
|
67
|
+
const advertisements = queue.advertisementList.get();
|
|
68
|
+
if (!advertisements?.size) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const advertisement = advertisements.values().next().value;
|
|
72
|
+
const advertisementData = advertisement.data.get();
|
|
73
|
+
if (!advertisementData.data && !advertisementData.error && !advertisementData.loading) {
|
|
74
|
+
void advertisement.promise();
|
|
75
|
+
eventBus.emit('poll', {
|
|
76
|
+
action: 'opened',
|
|
77
|
+
payload: {
|
|
78
|
+
questionId: advertisement.id,
|
|
79
|
+
questionType: QuestionType.PROMOTION,
|
|
80
|
+
questionOpenedFrom: 'notification', // ToDo: add openedFrom to notification
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
markAsViewed(advertisement.id);
|
|
84
|
+
}
|
|
85
|
+
return advertisement;
|
|
86
|
+
};
|
|
87
|
+
onMount(queue.advertisementList, () => {
|
|
88
|
+
$advertisementList.subscribe((list) => {
|
|
89
|
+
if (list) {
|
|
90
|
+
const last = list[list.length - 1];
|
|
91
|
+
show(last.id);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
$feedSubscription.addListener('promotion', (response) => {
|
|
95
|
+
const feedItem = response.data?.attributes?.feedItem;
|
|
96
|
+
if (feedItem?.attributes?.attributes.case !== 'promotion') {
|
|
97
|
+
logger.debug('not promotion');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (feedItem.attributes.status === QuestionStatus.RESOLVED) {
|
|
101
|
+
hide(feedItem.id);
|
|
102
|
+
logger.debug({ feedItem }, 'resolved: %o');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (feedItem.attributes.status === QuestionStatus.ACTIVE) {
|
|
106
|
+
logger.debug({ feedItem }, 'active: %o');
|
|
107
|
+
show(feedItem.id);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
logger.debug({ feedItem }, 'skip: %o');
|
|
111
|
+
return;
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
// return () => {
|
|
115
|
+
// queue.drain()
|
|
116
|
+
// }
|
|
117
|
+
});
|
|
118
|
+
return { hide, show, $list: queue.advertisementList, getActiveAdvertisement };
|
|
119
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SingleStore, createComputedStore } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import { Advertisement } from '.';
|
|
3
|
+
export type AdvertisementsQueueOptions = {
|
|
4
|
+
concurrency: number;
|
|
5
|
+
animationDelay: number;
|
|
6
|
+
};
|
|
7
|
+
export type AdvertisementsList = ReturnType<typeof createComputedStore<Advertisement[]>>;
|
|
8
|
+
export declare class AdvertisementsQueue {
|
|
9
|
+
advertisementList: ReturnType<SingleStore<Map<Advertisement['id'], Advertisement>>['getStore']>;
|
|
10
|
+
private store;
|
|
11
|
+
private timeouts;
|
|
12
|
+
private waitingQueue;
|
|
13
|
+
private activeQueue;
|
|
14
|
+
private options;
|
|
15
|
+
private logger;
|
|
16
|
+
constructor(options: AdvertisementsQueueOptions);
|
|
17
|
+
addToQueue: (advertisement: Advertisement) => void;
|
|
18
|
+
tickWaitingQueue: () => void;
|
|
19
|
+
tickActiveQueue: (advertisementId: string) => void;
|
|
20
|
+
closeAdvertisement: (advertisementId: string, { animateHiding }?: {
|
|
21
|
+
animateHiding?: boolean | undefined;
|
|
22
|
+
}) => Advertisement | undefined;
|
|
23
|
+
drain: () => void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { createSingleStore } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
3
|
+
export class AdvertisementsQueue {
|
|
4
|
+
advertisementList;
|
|
5
|
+
store;
|
|
6
|
+
timeouts;
|
|
7
|
+
waitingQueue;
|
|
8
|
+
activeQueue;
|
|
9
|
+
options;
|
|
10
|
+
logger;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.options = options;
|
|
13
|
+
this.logger = createLogger('advertisement_queue');
|
|
14
|
+
this.store = new Map();
|
|
15
|
+
this.timeouts = new Map();
|
|
16
|
+
this.waitingQueue = new Set();
|
|
17
|
+
this.activeQueue = new Set();
|
|
18
|
+
this.advertisementList = createSingleStore(new Map());
|
|
19
|
+
}
|
|
20
|
+
addToQueue = (advertisement) => {
|
|
21
|
+
if (this.store.has(advertisement.id)) {
|
|
22
|
+
this.logger.debug({ advertisement }, 'skip existed advertisement: %o');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const close = advertisement.close;
|
|
26
|
+
const action = advertisement.action;
|
|
27
|
+
this.store.set(advertisement.id, {
|
|
28
|
+
...advertisement,
|
|
29
|
+
close: (...args) => {
|
|
30
|
+
if (close) {
|
|
31
|
+
close(...args);
|
|
32
|
+
}
|
|
33
|
+
this.closeAdvertisement(advertisement.id);
|
|
34
|
+
},
|
|
35
|
+
action: (...args) => {
|
|
36
|
+
if (action) {
|
|
37
|
+
action(...args);
|
|
38
|
+
}
|
|
39
|
+
this.closeAdvertisement(advertisement.id, { animateHiding: false });
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
this.waitingQueue.add(advertisement.id);
|
|
43
|
+
/**
|
|
44
|
+
* Hide an oldest advertisement if the active queue is full,
|
|
45
|
+
* and show a new advertisement on the next tick
|
|
46
|
+
*/
|
|
47
|
+
if (this.activeQueue.size === this.options.concurrency) {
|
|
48
|
+
const [job] = this.activeQueue;
|
|
49
|
+
this.closeAdvertisement(job);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
this.tickWaitingQueue();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
tickWaitingQueue = () => {
|
|
56
|
+
if (this.activeQueue.size < this.options.concurrency) {
|
|
57
|
+
const [job] = this.waitingQueue;
|
|
58
|
+
if (!job) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.activeQueue.add(job);
|
|
62
|
+
this.waitingQueue.delete(job);
|
|
63
|
+
this.logger.debug({ job }, 'waiting queue tick');
|
|
64
|
+
this.tickActiveQueue(job);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
this.logger.debug({ queueSize: this.activeQueue.size, concurrency: this.options.concurrency }, 'waiting queue tick skipped');
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
tickActiveQueue = (advertisementId) => {
|
|
71
|
+
if (!advertisementId) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const advertisement = this.store.get(advertisementId);
|
|
75
|
+
if (!advertisement) {
|
|
76
|
+
this.logger.debug({ advertisementId }, 'active queue tick skipped, advertisement not exist');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const timeout = setTimeout(() => {
|
|
80
|
+
const closureId = advertisementId;
|
|
81
|
+
const prevQueue = new Map(this.advertisementList.get());
|
|
82
|
+
prevQueue.set(advertisement.id, advertisement);
|
|
83
|
+
this.advertisementList.set(prevQueue);
|
|
84
|
+
if (advertisement.autoHideDuration !== Infinity) {
|
|
85
|
+
const timeout = setTimeout(() => {
|
|
86
|
+
this.logger.debug({ advertisementId: closureId, delay: advertisement.autoHideDuration || 5000 }, 'advertisement hiding by autoHideDuration');
|
|
87
|
+
this.closeAdvertisement(closureId);
|
|
88
|
+
}, advertisement.autoHideDuration || 5000);
|
|
89
|
+
this.timeouts.set(closureId, timeout);
|
|
90
|
+
}
|
|
91
|
+
this.logger.debug({ advertisementId: closureId, queue: [...prevQueue.values()] }, 'advertisement displayed');
|
|
92
|
+
}, advertisement.delay || 0);
|
|
93
|
+
this.timeouts.set(advertisementId, timeout);
|
|
94
|
+
this.logger.debug({ advertisementId }, 'active queue tick completed');
|
|
95
|
+
this.tickWaitingQueue();
|
|
96
|
+
};
|
|
97
|
+
closeAdvertisement = (advertisementId, { animateHiding = true } = {}) => {
|
|
98
|
+
const prevQueue = new Map(this.advertisementList.get());
|
|
99
|
+
const advertisement = prevQueue.get(advertisementId);
|
|
100
|
+
if (advertisement) {
|
|
101
|
+
// do not hide advertisement if we have more than one advertisement in waiting queue,
|
|
102
|
+
// because we need to show next advertisement immediately
|
|
103
|
+
advertisement.hiding = !(this.waitingQueue.size >= this.options.concurrency) && animateHiding;
|
|
104
|
+
this.advertisementList.set(prevQueue);
|
|
105
|
+
const timeout = setTimeout(() => {
|
|
106
|
+
const prevQueue = new Map(this.advertisementList.get());
|
|
107
|
+
prevQueue.delete(advertisementId);
|
|
108
|
+
this.advertisementList.set(prevQueue);
|
|
109
|
+
const timeout = this.timeouts.get(advertisementId);
|
|
110
|
+
if (timeout !== undefined) {
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
this.timeouts.delete(advertisementId);
|
|
113
|
+
}
|
|
114
|
+
this.logger.debug({ advertisementId }, 'advertisement hidden');
|
|
115
|
+
}, this.options.animationDelay || 0);
|
|
116
|
+
this.timeouts.set(advertisementId, timeout);
|
|
117
|
+
}
|
|
118
|
+
this.store.delete(advertisementId);
|
|
119
|
+
this.activeQueue.delete(advertisementId);
|
|
120
|
+
this.waitingQueue.delete(advertisementId);
|
|
121
|
+
this.tickWaitingQueue();
|
|
122
|
+
this.logger.debug({ advertisementId }, 'advertisement hiding');
|
|
123
|
+
return advertisement;
|
|
124
|
+
};
|
|
125
|
+
drain = () => {
|
|
126
|
+
this.store.clear();
|
|
127
|
+
this.timeouts.clear();
|
|
128
|
+
this.waitingQueue.clear();
|
|
129
|
+
this.activeQueue.clear();
|
|
130
|
+
this.advertisementList.off();
|
|
131
|
+
this.advertisementList.set(new Map());
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Storage } from '@streamlayer/sdk-web-storage';
|
|
2
|
+
var KEY_PREFIX;
|
|
3
|
+
(function (KEY_PREFIX) {
|
|
4
|
+
KEY_PREFIX["SHOWED"] = "showed";
|
|
5
|
+
})(KEY_PREFIX || (KEY_PREFIX = {}));
|
|
6
|
+
export class AdvertisementStorage extends Storage {
|
|
7
|
+
constructor() {
|
|
8
|
+
super('advertisement');
|
|
9
|
+
}
|
|
10
|
+
setShowed = (advertId) => {
|
|
11
|
+
this.write(KEY_PREFIX.SHOWED, advertId, 'true');
|
|
12
|
+
};
|
|
13
|
+
isShowed = (advertId) => {
|
|
14
|
+
return this.read(KEY_PREFIX.SHOWED, advertId);
|
|
15
|
+
};
|
|
16
|
+
}
|
package/lib/background.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import '@streamlayer/sdk-web-core/store';
|
|
|
5
5
|
import { ReadableAtom, WritableAtom } from 'nanostores';
|
|
6
6
|
import * as queries from './queries';
|
|
7
7
|
import { detail } from './detail';
|
|
8
|
+
import { advertisement } from './advertisement';
|
|
8
9
|
export declare enum InteractiveAllowed {
|
|
9
10
|
ALLOWED = "allowed",
|
|
10
11
|
DISALLOWED = "disallowed"
|
|
@@ -44,6 +45,7 @@ export declare class GamificationBackground {
|
|
|
44
45
|
feedSubscription: ReturnType<typeof queries.feedSubscription>;
|
|
45
46
|
/** subscription to opened question (vote percentage) */
|
|
46
47
|
questionSubscription?: ReturnType<typeof queries.questionSubscription>;
|
|
48
|
+
advertisement: ReturnType<typeof advertisement>;
|
|
47
49
|
private notifications;
|
|
48
50
|
private log;
|
|
49
51
|
private transport;
|
|
@@ -69,7 +71,7 @@ export declare class GamificationBackground {
|
|
|
69
71
|
* Open question and mark notification for this question as viewed
|
|
70
72
|
*/
|
|
71
73
|
openQuestion: (questionId: string, question?: FeedItem & {
|
|
72
|
-
openedFrom?:
|
|
74
|
+
openedFrom?: "list" | "notification";
|
|
73
75
|
}) => void;
|
|
74
76
|
/**
|
|
75
77
|
* Close question and mark notification for this question as viewed
|
package/lib/background.js
CHANGED
|
@@ -4,6 +4,7 @@ import { QuestionStatus } from '@streamlayer/sdk-web-types';
|
|
|
4
4
|
import '@streamlayer/sdk-web-core/store';
|
|
5
5
|
import * as queries from './queries';
|
|
6
6
|
import { detail } from './detail';
|
|
7
|
+
import { advertisement } from './advertisement';
|
|
7
8
|
export var InteractiveAllowed;
|
|
8
9
|
(function (InteractiveAllowed) {
|
|
9
10
|
InteractiveAllowed["ALLOWED"] = "allowed";
|
|
@@ -39,6 +40,7 @@ export class GamificationBackground {
|
|
|
39
40
|
feedSubscription;
|
|
40
41
|
/** subscription to opened question (vote percentage) */
|
|
41
42
|
questionSubscription;
|
|
43
|
+
advertisement;
|
|
42
44
|
notifications;
|
|
43
45
|
log;
|
|
44
46
|
transport;
|
|
@@ -118,6 +120,7 @@ export class GamificationBackground {
|
|
|
118
120
|
}
|
|
119
121
|
};
|
|
120
122
|
});
|
|
123
|
+
this.advertisement = advertisement(this.feedList, this.feedSubscription, instance.transport);
|
|
121
124
|
}
|
|
122
125
|
/**
|
|
123
126
|
* Get id for notifications and link with current session
|
package/lib/deepLink.js
CHANGED
|
@@ -34,11 +34,20 @@ export const deepLink = (transport, $eventId, $externalEventId, $userId) => {
|
|
|
34
34
|
hash = `#${createUrlParams('', urlParamsData)}`;
|
|
35
35
|
}
|
|
36
36
|
const desktopLink = window.location.origin + window.location.pathname + search + hash;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
try {
|
|
38
|
+
const shortLink = await generateShortLink(transport, { web: desktopLink, mobile: mobileDeepLink });
|
|
39
|
+
$store.set({
|
|
40
|
+
data: shortLink.data?.link,
|
|
41
|
+
loading: false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
$store.set({
|
|
46
|
+
loading: false,
|
|
47
|
+
error: 'Failed to generate short link',
|
|
48
|
+
data: undefined,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
42
51
|
}
|
|
43
52
|
else {
|
|
44
53
|
$store.setKey('loading', true);
|
package/lib/detail.d.ts
CHANGED
|
@@ -5,11 +5,11 @@ import { type GamificationBackground } from './background';
|
|
|
5
5
|
export declare const detail: (transport: Transport, $openedQuestionId: ReadableAtom<{
|
|
6
6
|
questionId: string;
|
|
7
7
|
question?: FeedItem & {
|
|
8
|
-
openedFrom?:
|
|
8
|
+
openedFrom?: "list" | "notification";
|
|
9
9
|
};
|
|
10
|
-
} | undefined>, $feedList: ReturnType<GamificationBackground[
|
|
10
|
+
} | undefined>, $feedList: ReturnType<GamificationBackground["feedList"]["getStore"]>) => {
|
|
11
11
|
$store: ReadableAtom<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedItem | (import("@bufbuild/protobuf").PlainMessage<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedItem> & {
|
|
12
|
-
openedFrom?: "list" | "notification"
|
|
12
|
+
openedFrom?: "list" | "notification";
|
|
13
13
|
}) | undefined>;
|
|
14
14
|
$extendedStore: import("@nanostores/query").FetcherStore<import("@bufbuild/protobuf").PlainMessage<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").ExtendedQuestion>, any>;
|
|
15
15
|
updateExtendedQuestion: (question: ExtendedQuestion | undefined) => void;
|
package/lib/friendSummary.d.ts
CHANGED
|
@@ -2,4 +2,4 @@ import type { Transport } from '@streamlayer/sdk-web-api';
|
|
|
2
2
|
import { ReadableAtom } from 'nanostores';
|
|
3
3
|
import { LeaderboardItem } from '@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb';
|
|
4
4
|
import { Gamification } from '.';
|
|
5
|
-
export declare const friendSummary: ($eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification[
|
|
5
|
+
export declare const friendSummary: ($eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification["friends"], friendId: string, transport: Transport) => Promise<LeaderboardItem | undefined>;
|
package/lib/gamification.d.ts
CHANGED
|
@@ -48,6 +48,7 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
|
|
|
48
48
|
feedSubscription: GamificationBackground['feedSubscription'];
|
|
49
49
|
activeQuestionId: GamificationBackground['activeQuestionId'];
|
|
50
50
|
openedQuestionId: GamificationBackground['openedQuestionId'];
|
|
51
|
+
advertisement: GamificationBackground['advertisement'];
|
|
51
52
|
private notifications;
|
|
52
53
|
private transport;
|
|
53
54
|
/** gamification background class, handle subscriptions and notifications for closed overlay */
|
|
@@ -63,7 +64,7 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
|
|
|
63
64
|
disconnect: () => void;
|
|
64
65
|
submitAnswer: (questionId: string, answerId: string) => Promise<void>;
|
|
65
66
|
openQuestion: (questionId?: string, question?: FeedItem & {
|
|
66
|
-
openedFrom?:
|
|
67
|
+
openedFrom?: "list" | "notification";
|
|
67
68
|
}) => void | (() => void);
|
|
68
69
|
closeQuestion: (questionId?: string) => void;
|
|
69
70
|
openUser: (friendId: string) => Promise<void>;
|
package/lib/gamification.js
CHANGED
|
@@ -49,6 +49,7 @@ export class Gamification extends AbstractFeature {
|
|
|
49
49
|
feedSubscription;
|
|
50
50
|
activeQuestionId;
|
|
51
51
|
openedQuestionId;
|
|
52
|
+
advertisement;
|
|
52
53
|
notifications;
|
|
53
54
|
transport;
|
|
54
55
|
/** gamification background class, handle subscriptions and notifications for closed overlay */
|
|
@@ -60,6 +61,7 @@ export class Gamification extends AbstractFeature {
|
|
|
60
61
|
constructor(config, source, instance) {
|
|
61
62
|
super(config, source);
|
|
62
63
|
this.background = new GamificationBackground(instance);
|
|
64
|
+
this.advertisement = this.background.advertisement;
|
|
63
65
|
this.feedSubscription = this.background.feedSubscription;
|
|
64
66
|
this.activeQuestionId = this.background.activeQuestionId;
|
|
65
67
|
this.openedQuestionId = this.background.openedQuestionId;
|
|
@@ -214,6 +216,10 @@ export class Gamification extends AbstractFeature {
|
|
|
214
216
|
if (!feedItem?.attributes) {
|
|
215
217
|
return;
|
|
216
218
|
}
|
|
219
|
+
// skip promotions, they are tracked in the advertisement
|
|
220
|
+
if (feedItem.attributes.attributes.case === 'promotion') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
217
223
|
// skip questions with status other than active or resolved
|
|
218
224
|
if (feedItem.attributes.status !== QuestionStatus.ACTIVE &&
|
|
219
225
|
feedItem.attributes.status !== QuestionStatus.RESOLVED) {
|
|
@@ -273,6 +279,7 @@ export class Gamification extends AbstractFeature {
|
|
|
273
279
|
}
|
|
274
280
|
if (questionIndex === -1) {
|
|
275
281
|
feedList.unshift(feedItem);
|
|
282
|
+
console.log('feedItem', feedItem);
|
|
276
283
|
eventBus.emit('poll', {
|
|
277
284
|
action: 'received',
|
|
278
285
|
payload: {
|
package/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { GamificationBackground } from './background';
|
|
2
2
|
export { Gamification } from './gamification';
|
|
3
|
+
export { Advertisement } from './advertisement';
|
|
3
4
|
declare module '@streamlayer/sdk-web-interfaces' {
|
|
4
5
|
interface StreamLayerContext {
|
|
5
6
|
gamification: import('./background').GamificationBackground;
|
package/lib/leaderboard.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ type LeaderboardStore = {
|
|
|
14
14
|
hasMore: boolean;
|
|
15
15
|
error?: string;
|
|
16
16
|
};
|
|
17
|
-
export declare const leaderboard: (transport: Transport, $eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification[
|
|
17
|
+
export declare const leaderboard: (transport: Transport, $eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification["friends"], options?: LeaderboardOptions) => {
|
|
18
18
|
$store: import("nanostores").MapStore<LeaderboardStore>;
|
|
19
19
|
fetchMore: (page?: number) => void;
|
|
20
20
|
invalidate: () => void;
|
package/lib/queries/deepLink.js
CHANGED
|
@@ -10,10 +10,15 @@ export const $deepLink = (transport, params) => {
|
|
|
10
10
|
if (!eventId) {
|
|
11
11
|
return {};
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
try {
|
|
14
|
+
const res = await client.generateDeepLink({
|
|
15
|
+
data: { gamification: true, externalEventId: externalEventId, eventId: eventId },
|
|
16
|
+
});
|
|
17
|
+
return res.data?.attributes;
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
17
22
|
},
|
|
18
23
|
dedupeTime: 1000 * 60 * 60 * 24, // 24 hours
|
|
19
24
|
refetchInterval: 0,
|
package/lib/queries/index.d.ts
CHANGED
|
@@ -109,7 +109,7 @@ export declare const feedSubscription: ($slStreamId: ReadableAtom<string | undef
|
|
|
109
109
|
readonly kind: import("@bufbuild/protobuf").MethodKind.ServerStreaming;
|
|
110
110
|
};
|
|
111
111
|
};
|
|
112
|
-
}, SubscriptionRequest, SubscriptionResponse, "subscription" | "votingSubscription" | "questionSubscription" | "feedSubscription", ((request: import("@bufbuild/protobuf").PartialMessage<SubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions
|
|
112
|
+
}, SubscriptionRequest, SubscriptionResponse, "subscription" | "votingSubscription" | "questionSubscription" | "feedSubscription", ((request: import("@bufbuild/protobuf").PartialMessage<SubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<SubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<VotingSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<VotingSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<QuestionSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<QuestionSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionResponse>)>;
|
|
113
113
|
export declare const votingSubscription: (params: {
|
|
114
114
|
questionId: string;
|
|
115
115
|
feedId: string;
|
|
@@ -219,7 +219,7 @@ export declare const votingSubscription: (params: {
|
|
|
219
219
|
readonly kind: import("@bufbuild/protobuf").MethodKind.ServerStreaming;
|
|
220
220
|
};
|
|
221
221
|
};
|
|
222
|
-
}, VotingSubscriptionRequest, VotingSubscriptionResponse, "subscription" | "votingSubscription" | "questionSubscription" | "feedSubscription", ((request: import("@bufbuild/protobuf").PartialMessage<SubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions
|
|
222
|
+
}, VotingSubscriptionRequest, VotingSubscriptionResponse, "subscription" | "votingSubscription" | "questionSubscription" | "feedSubscription", ((request: import("@bufbuild/protobuf").PartialMessage<SubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<SubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<VotingSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<VotingSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<QuestionSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<QuestionSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionResponse>)>;
|
|
223
223
|
export declare const questionSubscription: (questionId: string, transport: Transport) => import("packages/sdk-web-api/lib/grpc/subscription").ServerStreamSubscription<import("@bufbuild/protobuf").ServiceType, import("@bufbuild/protobuf").Message<import("@bufbuild/protobuf").AnyMessage>, import("@bufbuild/protobuf").Message<import("@bufbuild/protobuf").AnyMessage>, never, never> | import("packages/sdk-web-api/lib/grpc/subscription").ServerStreamSubscription<{
|
|
224
224
|
readonly typeName: "streamlayer.interactive.feed.Feed";
|
|
225
225
|
readonly methods: {
|
|
@@ -326,10 +326,11 @@ export declare const questionSubscription: (questionId: string, transport: Trans
|
|
|
326
326
|
readonly kind: import("@bufbuild/protobuf").MethodKind.ServerStreaming;
|
|
327
327
|
};
|
|
328
328
|
};
|
|
329
|
-
}, QuestionSubscriptionRequest, QuestionSubscriptionResponse, "subscription" | "votingSubscription" | "questionSubscription" | "feedSubscription", ((request: import("@bufbuild/protobuf").PartialMessage<SubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions
|
|
329
|
+
}, QuestionSubscriptionRequest, QuestionSubscriptionResponse, "subscription" | "votingSubscription" | "questionSubscription" | "feedSubscription", ((request: import("@bufbuild/protobuf").PartialMessage<SubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<SubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<VotingSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<VotingSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<QuestionSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<QuestionSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions) => AsyncIterable<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionResponse>)>;
|
|
330
330
|
export declare const getQuestionByUser: (questionId: string, transport: Transport) => Promise<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").ExtendedQuestion | undefined>;
|
|
331
331
|
export declare const getQuestionDetail: (questionId: string, transport: Transport) => Promise<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Question | undefined>;
|
|
332
332
|
export declare const $questionByUser: ($questionId: ReadableAtom<string | undefined> | string, transport: Transport) => import("@nanostores/query").FetcherStore<import("@bufbuild/protobuf").PlainMessage<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").ExtendedQuestion>, any>;
|
|
333
|
+
export declare const getPromotionDetail: (promoId: string, transport: Transport) => Promise<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionOptions_PromotionOptions | undefined>;
|
|
333
334
|
export declare const $pickHistory: (slStreamId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<(import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").PickHistory | undefined)[], any>;
|
|
334
335
|
export declare const $feedList: ($slStreamId: ReadableAtom<string | undefined>, $interactiveAllowed: ReadableAtom<InteractiveAllowed>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedItem[], any>;
|
|
335
336
|
export { $userSummary, $leaderboardList } from './leaderboard';
|
package/lib/queries/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export const $activeQuestion = (slStreamId, transport) => {
|
|
|
21
21
|
});
|
|
22
22
|
};
|
|
23
23
|
export const feedSubscription = ($slStreamId, transport) => {
|
|
24
|
+
console.log('feedSubscription', $slStreamId);
|
|
24
25
|
const { client } = transport.createStreamClient(Feed);
|
|
25
26
|
const params = atom({ eventId: $slStreamId.get() || '', feedId: '' });
|
|
26
27
|
$slStreamId.subscribe((eventId = '') => {
|
|
@@ -65,6 +66,19 @@ export const $questionByUser = ($questionId, transport) => {
|
|
|
65
66
|
dedupeTime: 1000 * 60 * 5,
|
|
66
67
|
});
|
|
67
68
|
};
|
|
69
|
+
export const getPromotionDetail = async (promoId, transport) => {
|
|
70
|
+
if (!promoId) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const { client } = transport.createPromiseClient(Feed, { method: 'getQuestion', params: [promoId] });
|
|
74
|
+
const res = await client.getQuestion({
|
|
75
|
+
id: promoId,
|
|
76
|
+
});
|
|
77
|
+
const promotions = res.data?.attributes?.options?.options.case === 'promotion'
|
|
78
|
+
? res.data?.attributes?.options?.options.value
|
|
79
|
+
: undefined;
|
|
80
|
+
return promotions;
|
|
81
|
+
};
|
|
68
82
|
export const $pickHistory = (slStreamId, transport) => {
|
|
69
83
|
const { client, queryKey } = transport.createPromiseClient(Feed, { method: 'pickHistory', params: [slStreamId] });
|
|
70
84
|
return transport.nanoquery.createFetcherStore(queryKey, {
|
package/lib/userSummary.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Transport } from '@streamlayer/sdk-web-api';
|
|
|
2
2
|
import { ReadableAtom } from 'nanostores';
|
|
3
3
|
import { LeaderboardSummaryItem } from '@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb';
|
|
4
4
|
import { Gamification } from '.';
|
|
5
|
-
export declare const summary: ($eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification[
|
|
5
|
+
export declare const summary: ($eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification["friends"], transport: Transport) => {
|
|
6
6
|
$store: import("nanostores").MapStore<LeaderboardSummaryItem | undefined>;
|
|
7
7
|
invalidate: () => void;
|
|
8
8
|
};
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamlayer/feature-gamification",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"peerDependencies": {
|
|
5
|
-
"@bufbuild/protobuf": "^1.
|
|
6
|
-
"@fastify/deepmerge": "^
|
|
7
|
-
"@streamlayer/sl-eslib": "^5.
|
|
8
|
-
"nanostores": "^0.10.
|
|
9
|
-
"@streamlayer/sdk-web-api": "^
|
|
10
|
-
"@streamlayer/sdk-web-core": "^0.
|
|
11
|
-
"@streamlayer/sdk-web-interfaces": "^0.
|
|
12
|
-
"@streamlayer/sdk-web-logger": "^0.
|
|
13
|
-
"@streamlayer/sdk-web-notifications": "^0.
|
|
14
|
-
"@streamlayer/sdk-web-storage": "^0.
|
|
15
|
-
"@streamlayer/sdk-web-types": "^
|
|
5
|
+
"@bufbuild/protobuf": "^1.10.0",
|
|
6
|
+
"@fastify/deepmerge": "^2.0.0",
|
|
7
|
+
"@streamlayer/sl-eslib": "^5.104.1",
|
|
8
|
+
"nanostores": "^0.10.3",
|
|
9
|
+
"@streamlayer/sdk-web-api": "^1.1.0",
|
|
10
|
+
"@streamlayer/sdk-web-core": "^1.0.1",
|
|
11
|
+
"@streamlayer/sdk-web-interfaces": "^1.0.1",
|
|
12
|
+
"@streamlayer/sdk-web-logger": "^1.0.1",
|
|
13
|
+
"@streamlayer/sdk-web-notifications": "^1.0.1",
|
|
14
|
+
"@streamlayer/sdk-web-storage": "^1.0.1",
|
|
15
|
+
"@streamlayer/sdk-web-types": "^1.1.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"tslib": "^2.6.
|
|
18
|
+
"tslib": "^2.6.3"
|
|
19
19
|
},
|
|
20
20
|
"type": "module",
|
|
21
21
|
"main": "./lib/index.js",
|