@streamlayer/feature-gamification 1.2.1 → 1.3.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 +5 -4
- package/lib/advertisement/index.js +41 -30
- package/lib/background.js +3 -3
- package/lib/gamification.d.ts +1 -0
- package/lib/gamification.js +10 -5
- package/lib/queries/friends.js +7 -2
- package/lib/queries/index.d.ts +8 -0
- package/lib/queries/index.js +33 -1
- package/package.json +10 -10
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createMapStore } from '@streamlayer/sdk-web-interfaces';
|
|
2
2
|
import { PromotionOptions } from '@streamlayer/sdk-web-types';
|
|
3
|
-
import type
|
|
3
|
+
import { type Transport } from '@streamlayer/sdk-web-api';
|
|
4
4
|
import { type GamificationBackground } from '../background';
|
|
5
|
+
import { getPromotionDetail } from '../queries';
|
|
5
6
|
type AdvertisementData = {
|
|
6
7
|
loading?: boolean;
|
|
7
8
|
error?: unknown;
|
|
@@ -29,10 +30,10 @@ export type Advertisement = {
|
|
|
29
30
|
* - we subscribe to $feedList, and show last advertisement from list
|
|
30
31
|
* - we subscribe to $feedSubscription, and show advertisement on activate
|
|
31
32
|
*/
|
|
32
|
-
export declare const advertisement: ($
|
|
33
|
+
export declare const advertisement: ($slStreamId: GamificationBackground["slStreamId"], $feedSubscription: GamificationBackground["feedSubscription"], transport: Transport) => {
|
|
33
34
|
hide: (notificationId: string) => void;
|
|
34
|
-
show: (advertisementId: string,
|
|
35
|
+
show: (advertisementId: string, data?: Awaited<ReturnType<typeof getPromotionDetail>>) => void;
|
|
35
36
|
$list: import("nanostores").WritableAtom<Map<string, Advertisement> | undefined>;
|
|
36
|
-
getActiveAdvertisement: () => Advertisement | null;
|
|
37
|
+
getActiveAdvertisement: (persistent?: boolean) => Advertisement | null;
|
|
37
38
|
};
|
|
38
39
|
export {};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { QuestionStatus
|
|
1
|
+
import { ApiStore, createMapStore, eventBus } from '@streamlayer/sdk-web-interfaces';
|
|
2
|
+
import { QuestionStatus } from '@streamlayer/sdk-web-types';
|
|
3
3
|
import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
4
4
|
import { onMount } from 'nanostores';
|
|
5
|
-
import { getPromotionDetail } from '../queries';
|
|
5
|
+
import { $promotionList, getPromotionDetail } from '../queries';
|
|
6
6
|
import { AdvertisementStorage } from './storage';
|
|
7
7
|
import { AdvertisementsQueue } from './queue';
|
|
8
8
|
/**
|
|
@@ -17,20 +17,22 @@ import { AdvertisementsQueue } from './queue';
|
|
|
17
17
|
* - we subscribe to $feedList, and show last advertisement from list
|
|
18
18
|
* - we subscribe to $feedSubscription, and show advertisement on activate
|
|
19
19
|
*/
|
|
20
|
-
export const advertisement = ($
|
|
20
|
+
export const advertisement = ($slStreamId, $feedSubscription, transport) => {
|
|
21
21
|
const logger = createLogger('advertisement_queue');
|
|
22
22
|
const queue = new AdvertisementsQueue({ concurrency: 1, animationDelay: 1000 });
|
|
23
23
|
const storage = new AdvertisementStorage();
|
|
24
|
-
const $advertisementList =
|
|
25
|
-
return feedList.data?.filter((item) => item.type === 'promotion');
|
|
26
|
-
});
|
|
24
|
+
const $advertisementList = new ApiStore($promotionList($slStreamId, transport), 'gamification:promotionList');
|
|
27
25
|
/**
|
|
28
26
|
* Show advertisement by id, if it was not showed before.
|
|
29
27
|
*/
|
|
30
|
-
const show = (advertisementId,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const show = (advertisementId, data) => {
|
|
29
|
+
eventBus.emit('advertisement', {
|
|
30
|
+
action: 'received',
|
|
31
|
+
payload: {
|
|
32
|
+
advertisementId,
|
|
33
|
+
advertisementType: data?.type,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
34
36
|
queue.addToQueue({
|
|
35
37
|
id: advertisementId,
|
|
36
38
|
autoHideDuration: Infinity,
|
|
@@ -38,7 +40,7 @@ export const advertisement = ($feedList, $feedSubscription, transport) => {
|
|
|
38
40
|
promise: async function () {
|
|
39
41
|
this.data.setKey('loading', true);
|
|
40
42
|
try {
|
|
41
|
-
const response = await getPromotionDetail(advertisementId, transport);
|
|
43
|
+
const response = data || (await getPromotionDetail(advertisementId, transport));
|
|
42
44
|
if (!response) {
|
|
43
45
|
this.data.setKey('error', new Error('No promotion found'));
|
|
44
46
|
}
|
|
@@ -63,48 +65,57 @@ export const advertisement = ($feedList, $feedSubscription, transport) => {
|
|
|
63
65
|
queue.closeAdvertisement(notificationId);
|
|
64
66
|
markAsViewed(notificationId);
|
|
65
67
|
};
|
|
66
|
-
const getActiveAdvertisement = () => {
|
|
68
|
+
const getActiveAdvertisement = (persistent) => {
|
|
67
69
|
const advertisements = queue.advertisementList.get();
|
|
68
70
|
if (!advertisements?.size) {
|
|
69
71
|
return null;
|
|
70
72
|
}
|
|
71
73
|
const advertisement = advertisements.values().next().value;
|
|
72
74
|
const advertisementData = advertisement.data.get();
|
|
75
|
+
if (!persistent && storage.isShowed(advertisement.id)) {
|
|
76
|
+
return getActiveAdvertisement(persistent);
|
|
77
|
+
}
|
|
73
78
|
if (!advertisementData.data && !advertisementData.error && !advertisementData.loading) {
|
|
74
79
|
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
80
|
}
|
|
81
|
+
eventBus.emit('advertisement', {
|
|
82
|
+
action: 'opened',
|
|
83
|
+
payload: {
|
|
84
|
+
advertisementId: advertisement.id,
|
|
85
|
+
advertisementType: advertisementData.data?.type,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
markAsViewed(advertisement.id);
|
|
85
89
|
return advertisement;
|
|
86
90
|
};
|
|
87
91
|
onMount(queue.advertisementList, () => {
|
|
88
92
|
$advertisementList.subscribe((list) => {
|
|
89
|
-
if (list) {
|
|
90
|
-
const last = list[list.length - 1];
|
|
91
|
-
|
|
93
|
+
if (list.data) {
|
|
94
|
+
const last = list.data[list.data.length - 1];
|
|
95
|
+
if (last) {
|
|
96
|
+
show(last.id);
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
});
|
|
94
100
|
$feedSubscription.addListener('promotion', (response) => {
|
|
95
|
-
const feedItem = response.data?.attributes?.feedItem
|
|
96
|
-
|
|
101
|
+
const feedItem = response.data?.attributes?.feedItem?.attributes?.attributes?.case === 'promotion'
|
|
102
|
+
? response.data.attributes.feedItem.attributes
|
|
103
|
+
: undefined;
|
|
104
|
+
const promotionItem = response.data?.attributes?.question?.options?.options.case === 'promotion'
|
|
105
|
+
? response.data.attributes.question.options.options.value
|
|
106
|
+
: undefined;
|
|
107
|
+
if (feedItem === undefined || promotionItem === undefined) {
|
|
97
108
|
logger.debug('not promotion');
|
|
98
109
|
return;
|
|
99
110
|
}
|
|
100
|
-
if (feedItem.
|
|
111
|
+
if (feedItem.status === QuestionStatus.RESOLVED) {
|
|
101
112
|
hide(feedItem.id);
|
|
102
113
|
logger.debug({ feedItem }, 'resolved: %o');
|
|
103
114
|
return;
|
|
104
115
|
}
|
|
105
|
-
if (feedItem.
|
|
116
|
+
if (feedItem.status === QuestionStatus.ACTIVE) {
|
|
106
117
|
logger.debug({ feedItem }, 'active: %o');
|
|
107
|
-
show(feedItem.id);
|
|
118
|
+
show(feedItem.id, promotionItem);
|
|
108
119
|
return;
|
|
109
120
|
}
|
|
110
121
|
logger.debug({ feedItem }, 'skip: %o');
|
package/lib/background.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiStore, SingleStore, createSingleStore } from '@streamlayer/sdk-web-interfaces';
|
|
2
2
|
import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
3
|
-
import { QuestionStatus } from '@streamlayer/sdk-web-types';
|
|
3
|
+
import { QuestionStatus, QuestionType } 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';
|
|
@@ -81,7 +81,7 @@ export class GamificationBackground {
|
|
|
81
81
|
this.cancels.add(this.feedSubscription.addListener('feed-subscription-active-question', (response) => {
|
|
82
82
|
const activeQuestionId = this.activeQuestionId.get().data?.question?.id;
|
|
83
83
|
const question = response.data?.attributes?.question;
|
|
84
|
-
if (!question) {
|
|
84
|
+
if (!question || question.type === QuestionType.PROMOTION) {
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
// skip update question, avoid race condition
|
|
@@ -120,7 +120,7 @@ export class GamificationBackground {
|
|
|
120
120
|
}
|
|
121
121
|
};
|
|
122
122
|
});
|
|
123
|
-
this.advertisement = advertisement(this.
|
|
123
|
+
this.advertisement = advertisement(this.slStreamId, this.feedSubscription, instance.transport);
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
126
|
* Get id for notifications and link with current session
|
package/lib/gamification.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
|
|
|
66
66
|
openQuestion: (questionId?: string, question?: FeedItem & {
|
|
67
67
|
openedFrom?: "list" | "notification";
|
|
68
68
|
}) => void | (() => void);
|
|
69
|
+
isOpenedQuestion: (questionId: string) => string | undefined;
|
|
69
70
|
closeQuestion: (questionId?: string) => void;
|
|
70
71
|
openUser: (friendId: string) => Promise<void>;
|
|
71
72
|
closeUser: () => void;
|
package/lib/gamification.js
CHANGED
|
@@ -3,6 +3,7 @@ import { AbstractFeature, ApiStore, SingleStore, createSingleStore, eventBus, }
|
|
|
3
3
|
import { QuestionStatus, QuestionType, FeatureType, SilenceSetting, PickHistoryStatus, } from '@streamlayer/sdk-web-types';
|
|
4
4
|
import { NotificationType } from '@streamlayer/sdk-web-notifications';
|
|
5
5
|
import '@streamlayer/sdk-web-core/store';
|
|
6
|
+
import { onMount } from 'nanostores';
|
|
6
7
|
import * as queries from './queries';
|
|
7
8
|
import * as actions from './queries/actions';
|
|
8
9
|
import { GamificationStorage } from './storage';
|
|
@@ -87,16 +88,18 @@ export class Gamification extends AbstractFeature {
|
|
|
87
88
|
this.leaderboardList.invalidate(); // verified, it's necessary
|
|
88
89
|
}
|
|
89
90
|
}));
|
|
90
|
-
/**
|
|
91
|
-
* listen for active question and show in-app notification
|
|
92
|
-
*/
|
|
93
|
-
this.cancels.add(this.background.activeQuestionId.listen(this.showInApp));
|
|
94
91
|
/**
|
|
95
92
|
* listen for onboarding status, moderation onboarding changes and opt-in settings
|
|
96
93
|
*/
|
|
97
94
|
this.cancels.add(this.onboardingStatus.$store.listen(this.checkInteractiveFlag));
|
|
98
95
|
this.cancels.add(this.background.moderation.getStore().listen(this.checkInteractiveFlag));
|
|
99
96
|
this.cancels.add(this.settings.subscribe(this.checkInteractiveFlag));
|
|
97
|
+
onMount(this.background.activeQuestionId, () => {
|
|
98
|
+
/**
|
|
99
|
+
* listen for active question and show in-app notification
|
|
100
|
+
*/
|
|
101
|
+
this.background.activeQuestionId.listen(this.showInApp);
|
|
102
|
+
});
|
|
100
103
|
instance.sdk.onMount({ name: 'gamification', clear: true }, () => {
|
|
101
104
|
return () => {
|
|
102
105
|
for (const cancel of this.cancels) {
|
|
@@ -279,7 +282,6 @@ export class Gamification extends AbstractFeature {
|
|
|
279
282
|
}
|
|
280
283
|
if (questionIndex === -1) {
|
|
281
284
|
feedList.unshift(feedItem);
|
|
282
|
-
console.log('feedItem', feedItem);
|
|
283
285
|
eventBus.emit('poll', {
|
|
284
286
|
action: 'received',
|
|
285
287
|
payload: {
|
|
@@ -395,6 +397,9 @@ export class Gamification extends AbstractFeature {
|
|
|
395
397
|
});
|
|
396
398
|
return this.background.openQuestion(questionId, question);
|
|
397
399
|
};
|
|
400
|
+
isOpenedQuestion = (questionId) => {
|
|
401
|
+
return this.notifications.isViewed(questionId);
|
|
402
|
+
};
|
|
398
403
|
closeQuestion = (questionId) => {
|
|
399
404
|
return this.background.closeQuestion(questionId);
|
|
400
405
|
};
|
package/lib/queries/friends.js
CHANGED
|
@@ -9,8 +9,13 @@ export const $friends = ($userId, transport) => {
|
|
|
9
9
|
if (!userId) {
|
|
10
10
|
return [];
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
try {
|
|
13
|
+
const res = await client.getFriends({});
|
|
14
|
+
return res.data;
|
|
15
|
+
}
|
|
16
|
+
catch (_err) {
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
14
19
|
},
|
|
15
20
|
});
|
|
16
21
|
};
|
package/lib/queries/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Transport } from '@streamlayer/sdk-web-api';
|
|
2
|
+
import { QuestionStatus, QuestionType } from '@streamlayer/sdk-web-types';
|
|
2
3
|
import { ReadableAtom } from 'nanostores';
|
|
3
4
|
import type { SubscriptionRequest, SubscriptionResponse, VotingSubscriptionRequest, VotingSubscriptionResponse, QuestionSubscriptionRequest, QuestionSubscriptionResponse } from '@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb';
|
|
4
5
|
import { InteractiveAllowed } from '../background';
|
|
@@ -333,6 +334,13 @@ export declare const $questionByUser: ($questionId: ReadableAtom<string | undefi
|
|
|
333
334
|
export declare const getPromotionDetail: (promoId: string, transport: Transport) => Promise<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionOptions_PromotionOptions | undefined>;
|
|
334
335
|
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>;
|
|
335
336
|
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>;
|
|
337
|
+
export declare const $promotionList: ($slStreamId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<({
|
|
338
|
+
attributes: import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").PromotionHistory;
|
|
339
|
+
id: string;
|
|
340
|
+
type: QuestionType;
|
|
341
|
+
status: QuestionStatus;
|
|
342
|
+
created: string;
|
|
343
|
+
} | undefined)[], any>;
|
|
336
344
|
export { $userSummary, $leaderboardList } from './leaderboard';
|
|
337
345
|
export { $friends } from './friends';
|
|
338
346
|
export { $moderation } from './moderation';
|
package/lib/queries/index.js
CHANGED
|
@@ -21,7 +21,6 @@ export const $activeQuestion = (slStreamId, transport) => {
|
|
|
21
21
|
});
|
|
22
22
|
};
|
|
23
23
|
export const feedSubscription = ($slStreamId, transport) => {
|
|
24
|
-
console.log('feedSubscription', $slStreamId);
|
|
25
24
|
const { client } = transport.createStreamClient(Feed);
|
|
26
25
|
const params = atom({ eventId: $slStreamId.get() || '', feedId: '' });
|
|
27
26
|
$slStreamId.subscribe((eventId = '') => {
|
|
@@ -118,6 +117,39 @@ export const $feedList = ($slStreamId, $interactiveAllowed, transport) => {
|
|
|
118
117
|
refetchInterval: 0,
|
|
119
118
|
});
|
|
120
119
|
};
|
|
120
|
+
export const $promotionList = ($slStreamId, transport) => {
|
|
121
|
+
const { client, queryKey } = transport.createPromiseClient(Feed, {
|
|
122
|
+
method: 'list',
|
|
123
|
+
params: [$slStreamId],
|
|
124
|
+
});
|
|
125
|
+
return transport.nanoquery.createFetcherStore(queryKey, {
|
|
126
|
+
fetcher: async (_, __, slStreamId) => {
|
|
127
|
+
if (!slStreamId) {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
const res = await client.list({
|
|
131
|
+
eventId: slStreamId,
|
|
132
|
+
filter: {
|
|
133
|
+
types: [QuestionType.PROMOTION],
|
|
134
|
+
statuses: [QuestionStatus.ACTIVE],
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
return res.data
|
|
138
|
+
.map(({ attributes }) => {
|
|
139
|
+
if (!attributes || attributes.attributes.case !== 'promotion') {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
...attributes,
|
|
144
|
+
attributes: attributes.attributes.value,
|
|
145
|
+
};
|
|
146
|
+
})
|
|
147
|
+
.filter(Boolean);
|
|
148
|
+
},
|
|
149
|
+
dedupeTime: 0,
|
|
150
|
+
refetchInterval: 0,
|
|
151
|
+
});
|
|
152
|
+
};
|
|
121
153
|
export { $userSummary, $leaderboardList } from './leaderboard';
|
|
122
154
|
export { $friends } from './friends';
|
|
123
155
|
export { $moderation } from './moderation';
|
package/package.json
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamlayer/feature-gamification",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"@bufbuild/protobuf": "^1.10.0",
|
|
6
6
|
"@fastify/deepmerge": "^2.0.0",
|
|
7
|
-
"@streamlayer/sl-eslib": "^5.
|
|
7
|
+
"@streamlayer/sl-eslib": "^5.117.0",
|
|
8
8
|
"nanostores": "^0.10.3",
|
|
9
|
-
"@streamlayer/sdk-web-api": "^1.
|
|
10
|
-
"@streamlayer/sdk-web-
|
|
11
|
-
"@streamlayer/sdk-web-
|
|
12
|
-
"@streamlayer/sdk-web-
|
|
13
|
-
"@streamlayer/sdk-web-notifications": "^1.0
|
|
14
|
-
"@streamlayer/sdk-web-storage": "^1.0.
|
|
15
|
-
"@streamlayer/sdk-web-types": "^1.
|
|
9
|
+
"@streamlayer/sdk-web-api": "^1.2.0",
|
|
10
|
+
"@streamlayer/sdk-web-core": "^1.1.0",
|
|
11
|
+
"@streamlayer/sdk-web-interfaces": "^1.1.0",
|
|
12
|
+
"@streamlayer/sdk-web-logger": "^1.0.5",
|
|
13
|
+
"@streamlayer/sdk-web-notifications": "^1.1.0",
|
|
14
|
+
"@streamlayer/sdk-web-storage": "^1.0.5",
|
|
15
|
+
"@streamlayer/sdk-web-types": "^1.2.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"tslib": "^2.
|
|
18
|
+
"tslib": "^2.7.0"
|
|
19
19
|
},
|
|
20
20
|
"type": "module",
|
|
21
21
|
"main": "./lib/index.js",
|