@streamlayer/feature-gamification 1.2.2 → 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.
@@ -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 { Transport } from '@streamlayer/sdk-web-api';
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: ($feedList: GamificationBackground["feedList"], $feedSubscription: GamificationBackground["feedSubscription"], transport: Transport) => {
33
+ export declare const advertisement: ($slStreamId: GamificationBackground["slStreamId"], $feedSubscription: GamificationBackground["feedSubscription"], transport: Transport) => {
33
34
  hide: (notificationId: string) => void;
34
- show: (advertisementId: string, skipCheck?: boolean) => void;
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 { createComputedStore, createMapStore, eventBus } from '@streamlayer/sdk-web-interfaces';
2
- import { QuestionStatus, QuestionType } from '@streamlayer/sdk-web-types';
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 = ($feedList, $feedSubscription, transport) => {
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 = createComputedStore($feedList.getStore(), (feedList) => {
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, skipCheck) => {
31
- if (storage.isShowed(advertisementId) && !skipCheck) {
32
- return;
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
- show(last.id);
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
- if (feedItem?.attributes?.attributes.case !== 'promotion') {
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.attributes.status === QuestionStatus.RESOLVED) {
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.attributes.status === QuestionStatus.ACTIVE) {
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.feedList, this.feedSubscription, instance.transport);
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
@@ -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;
@@ -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
  };
@@ -9,8 +9,13 @@ export const $friends = ($userId, transport) => {
9
9
  if (!userId) {
10
10
  return [];
11
11
  }
12
- const res = await client.getFriends({});
13
- return res.data;
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
  };
@@ -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';
@@ -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.2.2",
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.104.1",
7
+ "@streamlayer/sl-eslib": "^5.117.0",
8
8
  "nanostores": "^0.10.3",
9
- "@streamlayer/sdk-web-api": "^1.1.3",
10
- "@streamlayer/sdk-web-core": "^1.0.4",
11
- "@streamlayer/sdk-web-interfaces": "^1.0.4",
12
- "@streamlayer/sdk-web-logger": "^1.0.4",
13
- "@streamlayer/sdk-web-notifications": "^1.0.4",
14
- "@streamlayer/sdk-web-storage": "^1.0.4",
15
- "@streamlayer/sdk-web-types": "^1.1.3"
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.6.3"
18
+ "tslib": "^2.7.0"
19
19
  },
20
20
  "type": "module",
21
21
  "main": "./lib/index.js",