@streamlayer/feature-gamification 1.5.4 → 1.6.1

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,25 +1,15 @@
1
- import { createMapStore, StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
2
- import { PromotionOptions } from '@streamlayer/sdk-web-types';
3
- import { QuestionNotification } from '@streamlayer/sl-eslib/interactive/interactive.common_pb';
1
+ import { StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
4
2
  import { type GamificationBackground } from '../background';
5
3
  import { getPromotionDetail } from '../queries';
6
- type AdvertisementData = {
7
- loading?: boolean;
8
- error?: unknown;
9
- data?: {
10
- promotions: PromotionOptions;
11
- notification?: QuestionNotification;
12
- };
13
- };
4
+ type AdvertisementData = Exclude<Awaited<ReturnType<typeof getPromotionDetail>>, undefined>;
14
5
  export type Advertisement = {
15
- autoHideDuration: number;
16
- delay?: number;
6
+ data?: AdvertisementData;
7
+ loading?: boolean;
17
8
  hiding?: boolean;
18
- action?: (...args: unknown[]) => void;
19
- close?: (...args: unknown[]) => void;
20
- data: ReturnType<typeof createMapStore<AdvertisementData>>;
21
- promise: () => Promise<void>;
22
- id: string;
9
+ isViewed?: boolean;
10
+ close?: () => void;
11
+ error?: string;
12
+ ctx?: Record<string, unknown>;
23
13
  };
24
14
  /**
25
15
  * @name Advertisement
@@ -34,9 +24,8 @@ export type Advertisement = {
34
24
  * - we subscribe to $feedSubscription, and show advertisement on activate
35
25
  */
36
26
  export declare const advertisement: ($slStreamId: GamificationBackground["slStreamId"], $feedSubscription: GamificationBackground["feedSubscription"], instance: StreamLayerContext) => {
37
- hide: (notificationId: string) => void;
27
+ hide: (notificationId?: string) => void;
38
28
  show: (advertisementId: string, data?: Awaited<ReturnType<typeof getPromotionDetail>>) => void;
39
- $list: import("nanostores").WritableAtom<Map<string, Advertisement> | undefined>;
40
- getActiveAdvertisement: (persistent?: boolean) => Advertisement | null;
29
+ $store: import("nanostores").MapStore<Advertisement>;
41
30
  };
42
31
  export {};
@@ -1,11 +1,10 @@
1
- import { ApiStore, createMapStore, eventBus } from '@streamlayer/sdk-web-interfaces';
1
+ import { createMapStore, eventBus } from '@streamlayer/sdk-web-interfaces';
2
2
  import { QuestionStatus } from '@streamlayer/sdk-web-types';
3
3
  import { createLogger } from '@streamlayer/sdk-web-logger';
4
- import { onMount } from 'nanostores';
5
4
  import { NotificationEnabled } from '@streamlayer/sl-eslib/interactive/interactive.common_pb';
6
- import { $promotionList, getPromotionDetail } from '../queries';
5
+ import { $activePromotionId, getPromotionDetail } from '../queries';
7
6
  import { AdvertisementStorage } from './storage';
8
- import { AdvertisementsQueue } from './queue';
7
+ import { parsePromotion } from './utils';
9
8
  /**
10
9
  * @name Advertisement
11
10
  * @description advertisement functionality, show, hide
@@ -20,159 +19,107 @@ import { AdvertisementsQueue } from './queue';
20
19
  */
21
20
  export const advertisement = ($slStreamId, $feedSubscription, instance) => {
22
21
  const transport = instance.transport;
23
- const logger = createLogger('advertisement_queue');
24
- const queue = new AdvertisementsQueue({ concurrency: 1, animationDelay: 1000 });
22
+ const logger = createLogger('advertisement');
25
23
  const storage = new AdvertisementStorage();
26
- const $advertisementList = new ApiStore($promotionList($slStreamId, transport), 'gamification:promotionList');
24
+ const $store = createMapStore({});
25
+ const $activeAdvertisement = $activePromotionId($slStreamId, transport);
27
26
  /**
28
- * Show advertisement by id, if it was not showed before.
27
+ * Show advertisement by id
29
28
  */
30
29
  const show = (advertisementId, data) => {
31
- eventBus.emit('advertisement', {
32
- action: 'received',
33
- payload: {
34
- advertisementId,
35
- advertisementType: data?.promotions.type,
36
- },
37
- });
38
- queue.addToQueue({
39
- id: advertisementId,
40
- autoHideDuration: Infinity,
41
- data: createMapStore({ loading: false, error: undefined, data: undefined }),
42
- close: () => hide(advertisementId),
43
- promise: async function () {
44
- this.data.setKey('loading', true);
45
- try {
46
- const response = data || (await getPromotionDetail(advertisementId, transport));
47
- instance.sdk.onAdvertisementActivate({
48
- stage: 'activate',
49
- id: advertisementId,
50
- hasNotification: response?.notification?.enabled === NotificationEnabled.NOTIFICATION_ENABLED,
51
- isShowed: !!storage.isShowed(advertisementId),
52
- });
53
- if (!response) {
54
- this.data.setKey('error', new Error('No promotion found'));
55
- }
56
- else {
57
- this.data.setKey('data', response);
58
- }
59
- }
60
- catch (error) {
61
- this.data.setKey('error', error);
62
- }
63
- finally {
64
- this.data.setKey('loading', false);
65
- }
66
- },
67
- });
30
+ if (!data) {
31
+ $store.setKey('loading', true);
32
+ void getPromotionDetail(advertisementId, transport)
33
+ .then((response) => $store.set({
34
+ loading: false,
35
+ error: undefined,
36
+ data: response,
37
+ close: () => hide(response?.question.id),
38
+ isViewed: response && !!storage.isViewed(response.question.id),
39
+ }))
40
+ .catch((error) => $store.set({
41
+ loading: false,
42
+ error,
43
+ data: undefined,
44
+ }));
45
+ }
46
+ else {
47
+ $store.set({
48
+ loading: false,
49
+ error: undefined,
50
+ data,
51
+ close: () => hide(data.question.id),
52
+ isViewed: !!storage.isViewed(data.question.id),
53
+ });
54
+ }
68
55
  };
56
+ $activeAdvertisement.subscribe((active, prevActive) => {
57
+ if (active.data) {
58
+ eventBus.emit('advertisement', {
59
+ action: 'received',
60
+ payload: {
61
+ advertisementId: active.data.question.id,
62
+ advertisementType: active.data?.promotion?.type,
63
+ },
64
+ });
65
+ if (!prevActive?.data || active.data.id !== prevActive.data.id) {
66
+ show(active.data.question.id, active.data);
67
+ }
68
+ }
69
+ });
70
+ $store.subscribe((active, prevActive) => {
71
+ if (active.data) {
72
+ instance.sdk.onAdvertisementActivate({
73
+ stage: 'activate',
74
+ id: active.data.question.id,
75
+ hasNotification: active.data.notification?.enabled === NotificationEnabled.NOTIFICATION_ENABLED,
76
+ isViewed: !!storage.isViewed(active.data.question.id),
77
+ });
78
+ eventBus.emit('advertisement', {
79
+ action: 'opened',
80
+ payload: {
81
+ advertisementId: active.data.question.id,
82
+ advertisementType: active.data?.promotion?.type,
83
+ },
84
+ });
85
+ }
86
+ if (!active?.data && prevActive?.data) {
87
+ instance.sdk.onAdvertisementActivate({
88
+ stage: 'deactivate',
89
+ id: prevActive.data.question.id,
90
+ isViewed: !!storage.isViewed(prevActive.data.question.id),
91
+ });
92
+ }
93
+ });
69
94
  const markAsViewed = (notificationId) => {
70
95
  logger.debug({ notificationId }, 'markAsViewed: %o');
71
96
  storage.setShowed(notificationId);
72
97
  };
73
98
  const hide = (notificationId) => {
74
- queue.closeAdvertisement(notificationId);
75
- markAsViewed(notificationId);
76
- instance.sdk.onAdvertisementActivate({
77
- stage: 'deactivate',
78
- id: notificationId,
79
- });
80
- };
81
- const getActiveAdvertisement = (persistent) => {
82
- const advertisements = queue.advertisementList.get();
83
- if (!advertisements?.size) {
84
- return null;
85
- }
86
- const advertisement = advertisements.values().next().value;
87
- const advertisementData = advertisement.data.get();
88
- if (!persistent && storage.isShowed(advertisement.id)) {
89
- queue.removeFromList(advertisement.id);
90
- return getActiveAdvertisement(persistent);
99
+ if (!notificationId || $store.get()?.data?.question.id === notificationId) {
100
+ $store.set({});
91
101
  }
92
- if (!advertisementData.data && !advertisementData.error && !advertisementData.loading) {
93
- void advertisement.promise();
102
+ if (notificationId) {
103
+ markAsViewed(notificationId);
94
104
  }
95
- eventBus.emit('advertisement', {
96
- action: 'opened',
97
- payload: {
98
- advertisementId: advertisement.id,
99
- advertisementType: advertisementData.data?.promotions.type,
100
- },
101
- });
102
- return advertisement;
103
105
  };
104
- $advertisementList.subscribe((list) => {
105
- if (list.data) {
106
- const last = list.data[list.data.length - 1];
107
- if (last) {
108
- show(last.id);
109
- }
110
- }
111
- });
112
- $feedSubscription.addListener('promotion cb', (response) => {
113
- const feedItem = response.data?.attributes?.feedItem?.attributes?.attributes?.case === 'promotion'
114
- ? response.data.attributes.feedItem.attributes
115
- : undefined;
116
- const promotionItem = response.data?.attributes?.question?.options?.options.case === 'promotion'
117
- ? response.data.attributes.question.options.options.value
118
- : undefined;
119
- if (feedItem === undefined || promotionItem === undefined) {
120
- logger.debug('not promotion');
106
+ $feedSubscription.addListener('promotion', (response) => {
107
+ const promotion = parsePromotion(response);
108
+ if (!promotion) {
121
109
  return;
122
110
  }
123
- if (feedItem.status === QuestionStatus.RESOLVED) {
124
- hide(feedItem.id);
125
- logger.debug({ feedItem }, 'resolved: %o');
111
+ if (promotion.question.status === QuestionStatus.RESOLVED) {
112
+ hide(promotion.question.id);
113
+ logger.debug({ promotion }, 'resolved: %o');
126
114
  return;
127
115
  }
128
- if (feedItem.status === QuestionStatus.ACTIVE) {
129
- logger.debug({ feedItem }, 'active: %o');
130
- show(feedItem.id);
116
+ if (promotion.question.status === QuestionStatus.ACTIVE) {
117
+ logger.debug({ promotion }, 'active: %o');
118
+ show(promotion.question.id, promotion);
131
119
  return;
132
120
  }
133
- logger.debug({ feedItem }, 'skip: %o');
134
- return;
135
- });
136
- onMount(queue.advertisementList, () => {
137
- $advertisementList.subscribe((list) => {
138
- if (list.data) {
139
- const last = list.data[list.data.length - 1];
140
- if (last) {
141
- show(last.id);
142
- }
143
- }
144
- });
145
- $feedSubscription.addListener('promotion', (response) => {
146
- const feedItem = response.data?.attributes?.feedItem?.attributes?.attributes?.case === 'promotion'
147
- ? response.data.attributes.feedItem.attributes
148
- : undefined;
149
- const promotionItem = response.data?.attributes?.question?.options?.options.case === 'promotion'
150
- ? response.data.attributes.question.options.options.value
151
- : undefined;
152
- if (feedItem === undefined || promotionItem === undefined) {
153
- logger.debug('not promotion');
154
- return;
155
- }
156
- if (feedItem.status === QuestionStatus.RESOLVED) {
157
- hide(feedItem.id);
158
- logger.debug({ feedItem }, 'resolved: %o');
159
- return;
160
- }
161
- if (feedItem.status === QuestionStatus.ACTIVE) {
162
- logger.debug({ feedItem }, 'active: %o');
163
- show(feedItem.id, {
164
- promotions: promotionItem,
165
- notification: response.data?.attributes?.question?.notification,
166
- });
167
- return;
168
- }
169
- logger.debug({ feedItem }, 'skip: %o');
170
- return;
171
- });
121
+ logger.debug({ promotion }, 'skip: %o');
172
122
  return;
173
- // return () => {
174
- // queue.drain()
175
- // }
176
123
  });
177
- return { hide, show, $list: queue.advertisementList, getActiveAdvertisement };
124
+ return { hide, show, $store };
178
125
  };
@@ -2,5 +2,5 @@ import { Storage } from '@streamlayer/sdk-web-storage';
2
2
  export declare class AdvertisementStorage extends Storage {
3
3
  constructor();
4
4
  setShowed: (advertId: string) => void;
5
- isShowed: (advertId: string) => string | undefined;
5
+ isViewed: (advertId: string) => string | undefined;
6
6
  }
@@ -10,7 +10,7 @@ export class AdvertisementStorage extends Storage {
10
10
  setShowed = (advertId) => {
11
11
  this.write(KEY_PREFIX.SHOWED, advertId, 'true');
12
12
  };
13
- isShowed = (advertId) => {
13
+ isViewed = (advertId) => {
14
14
  return this.read(KEY_PREFIX.SHOWED, advertId);
15
15
  };
16
16
  }
@@ -0,0 +1,31 @@
1
+ import { SubscriptionResponse } from '@streamlayer/sdk-web-types';
2
+ export declare const parsePromotion: (response: SubscriptionResponse) => {
3
+ id: string;
4
+ question: {
5
+ id: string;
6
+ type: import("@streamlayer/sdk-web-types").QuestionType;
7
+ subject?: string;
8
+ appearance?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAppearance;
9
+ sponsorship?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Sponsorship;
10
+ answers: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAnswer[];
11
+ status: import("@streamlayer/sdk-web-types").QuestionStatus;
12
+ position?: number;
13
+ marketClosed: boolean;
14
+ activatedAt: string;
15
+ answerSetAt: string;
16
+ overrides: {
17
+ [key: string]: boolean;
18
+ };
19
+ eventId: string;
20
+ streamTimestamp?: import("@bufbuild/protobuf").Timestamp;
21
+ tags: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Tag[];
22
+ bypassNotifications?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").BypassNotifications;
23
+ importId: string;
24
+ activationTriggerCount?: number;
25
+ source: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionSource;
26
+ ai?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAi;
27
+ hasTriggers?: boolean;
28
+ };
29
+ notification: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionNotification | undefined;
30
+ promotion: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionOptions_PromotionOptions | undefined;
31
+ } | undefined;
@@ -0,0 +1,16 @@
1
+ export const parsePromotion = (response) => {
2
+ const questionItem = response.data?.attributes?.question;
3
+ const feedItem = response.data?.attributes?.feedItem?.type === 'promotion' ? response.data.attributes.feedItem : undefined;
4
+ if (feedItem === undefined || !questionItem) {
5
+ return;
6
+ }
7
+ const { options, notification, ...question } = questionItem;
8
+ const promotionItem = options?.options.case === 'promotion' ? options.options.value : undefined;
9
+ // eslint-disable-next-line consistent-return
10
+ return {
11
+ id: question.id,
12
+ question,
13
+ notification,
14
+ promotion: promotionItem,
15
+ };
16
+ };
package/lib/background.js CHANGED
@@ -84,8 +84,10 @@ export class GamificationBackground {
84
84
  if (!question || question.type === QuestionType.PROMOTION) {
85
85
  return;
86
86
  }
87
- // skip update question, avoid race condition
88
- if (activeQuestionId && question.status === QuestionStatus.RESOLVED && question.id !== activeQuestionId) {
87
+ if (question.status === QuestionStatus.RESOLVED) {
88
+ if (activeQuestionId === question.id) {
89
+ this.activeQuestionId.mutate(undefined);
90
+ }
89
91
  return;
90
92
  }
91
93
  this.activeQuestionId.mutate(response.data?.attributes);
@@ -100,6 +102,26 @@ export class GamificationBackground {
100
102
  }
101
103
  });
102
104
  }));
105
+ this.cancels.add(this.activeQuestionId.subscribe((item, prevItem) => {
106
+ if (item.data?.feedItem) {
107
+ instance.sdk.onQuestionActivate({
108
+ stage: 'activate',
109
+ id: item.data.feedItem.id,
110
+ isViewed: !!this.notifications.isViewed(item.data.feedItem.id),
111
+ hasNotification: true,
112
+ type: item.data.feedItem.type,
113
+ });
114
+ }
115
+ if (!item.data?.feedItem && prevItem?.data?.feedItem) {
116
+ instance.sdk.onQuestionActivate({
117
+ stage: 'deactivate',
118
+ id: prevItem.data.feedItem.id,
119
+ isViewed: !!this.notifications.isViewed(prevItem.data.feedItem.id),
120
+ hasNotification: true,
121
+ type: prevItem.data.feedItem.type,
122
+ });
123
+ }
124
+ }));
103
125
  this.feedSubscription.connect();
104
126
  /**
105
127
  * invalidate active question on interactiveAllowed change
@@ -67,7 +67,7 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
67
67
  openedFrom?: "list" | "notification";
68
68
  }) => void | (() => void);
69
69
  getFeedItem: (id: string) => Promise<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedItem | undefined>;
70
- isOpenedQuestion: (questionId: string) => string | undefined;
70
+ isOpenedQuestion: (questionId: string) => boolean;
71
71
  closeQuestion: (questionId?: string) => void;
72
72
  openUser: (friendId: string) => Promise<void>;
73
73
  closeUser: () => void;
@@ -401,7 +401,7 @@ export class Gamification extends AbstractFeature {
401
401
  return queries.getFeedItem(id, this.transport);
402
402
  };
403
403
  isOpenedQuestion = (questionId) => {
404
- return this.notifications.isViewed(questionId);
404
+ return !!this.notifications.isViewed(questionId);
405
405
  };
406
406
  closeQuestion = (questionId) => {
407
407
  return this.background.closeQuestion(questionId);
@@ -351,18 +351,67 @@ export declare const getQuestionByUser: (questionId: string, transport: Transpor
351
351
  export declare const getQuestionDetail: (questionId: string, transport: Transport) => Promise<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Question | undefined>;
352
352
  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>;
353
353
  export declare const getPromotionDetail: (promoId: string, transport: Transport) => Promise<{
354
- promotions: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionOptions_PromotionOptions;
354
+ id: string;
355
+ question: {
356
+ id: string;
357
+ type: QuestionType;
358
+ subject?: string;
359
+ appearance?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAppearance;
360
+ sponsorship?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Sponsorship;
361
+ answers: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAnswer[];
362
+ status: QuestionStatus;
363
+ position?: number;
364
+ marketClosed: boolean;
365
+ activatedAt: string;
366
+ answerSetAt: string;
367
+ overrides: {
368
+ [key: string]: boolean;
369
+ };
370
+ eventId: string;
371
+ streamTimestamp?: import("@bufbuild/protobuf").Timestamp;
372
+ tags: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Tag[];
373
+ bypassNotifications?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").BypassNotifications;
374
+ importId: string;
375
+ activationTriggerCount?: number;
376
+ source: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionSource;
377
+ ai?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAi;
378
+ hasTriggers?: boolean;
379
+ };
380
+ promotion: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionOptions_PromotionOptions | undefined;
355
381
  notification: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionNotification | undefined;
356
382
  } | undefined>;
357
383
  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>;
358
384
  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>;
359
- export declare const $promotionList: ($slStreamId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<({
360
- attributes: import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").PromotionHistory;
385
+ export declare const $activePromotionId: ($slStreamId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<{
361
386
  id: string;
362
- type: QuestionType;
363
- status: QuestionStatus;
364
- created: string;
365
- } | undefined)[], any>;
387
+ question: {
388
+ id: string;
389
+ type: QuestionType;
390
+ subject?: string;
391
+ appearance?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAppearance;
392
+ sponsorship?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Sponsorship;
393
+ answers: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAnswer[];
394
+ status: QuestionStatus;
395
+ position?: number;
396
+ marketClosed: boolean;
397
+ activatedAt: string;
398
+ answerSetAt: string;
399
+ overrides: {
400
+ [key: string]: boolean;
401
+ };
402
+ eventId: string;
403
+ streamTimestamp?: import("@bufbuild/protobuf").Timestamp;
404
+ tags: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Tag[];
405
+ bypassNotifications?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").BypassNotifications;
406
+ importId: string;
407
+ activationTriggerCount?: number;
408
+ source: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionSource;
409
+ ai?: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionAi;
410
+ hasTriggers?: boolean;
411
+ };
412
+ promotion: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionOptions_PromotionOptions | undefined;
413
+ notification: import("@streamlayer/sl-eslib/interactive/interactive.common_pb").QuestionNotification | undefined;
414
+ } | undefined, any>;
366
415
  export { $userSummary, $leaderboardList } from './leaderboard';
367
416
  export { $friends } from './friends';
368
417
  export { $moderation } from './moderation';
@@ -80,10 +80,12 @@ export const getPromotionDetail = async (promoId, transport) => {
80
80
  const res = await client.getQuestion({
81
81
  id: promoId,
82
82
  });
83
- const promotions = res.data?.attributes?.options?.options.case === 'promotion'
84
- ? res.data?.attributes?.options?.options.value
85
- : undefined;
86
- return promotions ? { promotions, notification: res.data?.attributes?.notification } : undefined;
83
+ if (!res.data?.attributes) {
84
+ return undefined;
85
+ }
86
+ const { options, notification, ...question } = res.data.attributes;
87
+ const promotion = options?.options.case === 'promotion' ? options?.options.value : undefined;
88
+ return { id: question.id, question, promotion, notification };
87
89
  };
88
90
  export const $pickHistory = (slStreamId, transport) => {
89
91
  const { client, queryKey } = transport.createPromiseClient(Feed, { method: 'pickHistory', params: [slStreamId] });
@@ -124,7 +126,7 @@ export const $feedList = ($slStreamId, $interactiveAllowed, transport) => {
124
126
  refetchInterval: 0,
125
127
  });
126
128
  };
127
- export const $promotionList = ($slStreamId, transport) => {
129
+ export const $activePromotionId = ($slStreamId, transport) => {
128
130
  const { client, queryKey } = transport.createPromiseClient(Feed, {
129
131
  method: 'list',
130
132
  params: [$slStreamId],
@@ -132,7 +134,7 @@ export const $promotionList = ($slStreamId, transport) => {
132
134
  return transport.nanoquery.createFetcherStore(queryKey, {
133
135
  fetcher: async (_, __, slStreamId) => {
134
136
  if (!slStreamId) {
135
- return [];
137
+ return undefined;
136
138
  }
137
139
  const res = await client.list({
138
140
  eventId: slStreamId,
@@ -140,18 +142,16 @@ export const $promotionList = ($slStreamId, transport) => {
140
142
  types: [QuestionType.PROMOTION],
141
143
  statuses: [QuestionStatus.ACTIVE],
142
144
  },
145
+ pagination: {
146
+ page: 0,
147
+ pageSize: 1,
148
+ },
143
149
  });
144
- return res.data
145
- .map(({ attributes }) => {
146
- if (!attributes || attributes.attributes.case !== 'promotion') {
147
- return undefined;
148
- }
149
- return {
150
- ...attributes,
151
- attributes: attributes.attributes.value,
152
- };
153
- })
154
- .filter(Boolean);
150
+ const feedItem = res.data?.[0];
151
+ if (feedItem.type === 'promotion') {
152
+ return getPromotionDetail(feedItem.id, transport);
153
+ }
154
+ return undefined;
155
155
  },
156
156
  dedupeTime: 0,
157
157
  refetchInterval: 0,
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@streamlayer/feature-gamification",
3
- "version": "1.5.4",
3
+ "version": "1.6.1",
4
4
  "peerDependencies": {
5
5
  "@bufbuild/protobuf": "^1.10.0",
6
6
  "@fastify/deepmerge": "^2.0.0",
7
- "@streamlayer/sl-eslib": "^5.122.0",
7
+ "@streamlayer/sl-eslib": "^5.123.1",
8
8
  "nanostores": "^0.10.3",
9
- "@streamlayer/sdk-web-api": "^1.4.1",
10
- "@streamlayer/sdk-web-core": "^1.3.1",
11
- "@streamlayer/sdk-web-interfaces": "^1.1.11",
12
- "@streamlayer/sdk-web-logger": "^1.0.16",
13
- "@streamlayer/sdk-web-notifications": "^1.1.11",
14
- "@streamlayer/sdk-web-storage": "^1.0.16",
15
- "@streamlayer/sdk-web-types": "^1.5.1"
9
+ "@streamlayer/sdk-web-api": "^1.5.1",
10
+ "@streamlayer/sdk-web-core": "^1.4.1",
11
+ "@streamlayer/sdk-web-interfaces": "^1.1.13",
12
+ "@streamlayer/sdk-web-logger": "^1.0.18",
13
+ "@streamlayer/sdk-web-storage": "^1.0.18",
14
+ "@streamlayer/sdk-web-notifications": "^1.1.13",
15
+ "@streamlayer/sdk-web-types": "^1.6.1"
16
16
  },
17
17
  "devDependencies": {
18
18
  "tslib": "^2.7.0"
@@ -1,25 +0,0 @@
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
- removeFromList: (advertisementId: string) => void;
21
- closeAdvertisement: (advertisementId: string, { animateHiding }?: {
22
- animateHiding?: boolean | undefined;
23
- }) => Advertisement | undefined;
24
- drain: () => void;
25
- }
@@ -1,142 +0,0 @@
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
- removeFromList = (advertisementId) => {
98
- const prevQueue = new Map(this.advertisementList.get());
99
- prevQueue.delete(advertisementId);
100
- this.advertisementList.set(prevQueue);
101
- this.store.delete(advertisementId);
102
- this.activeQueue.delete(advertisementId);
103
- this.waitingQueue.delete(advertisementId);
104
- this.logger.debug({ advertisementId }, 'advertisement removed from list');
105
- };
106
- closeAdvertisement = (advertisementId, { animateHiding = true } = {}) => {
107
- const prevQueue = new Map(this.advertisementList.get());
108
- const advertisement = prevQueue.get(advertisementId);
109
- if (advertisement) {
110
- // do not hide advertisement if we have more than one advertisement in waiting queue,
111
- // because we need to show next advertisement immediately
112
- advertisement.hiding = !(this.waitingQueue.size >= this.options.concurrency) && animateHiding;
113
- this.advertisementList.set(prevQueue);
114
- const timeout = setTimeout(() => {
115
- const prevQueue = new Map(this.advertisementList.get());
116
- prevQueue.delete(advertisementId);
117
- this.advertisementList.set(prevQueue);
118
- const timeout = this.timeouts.get(advertisementId);
119
- if (timeout !== undefined) {
120
- clearTimeout(timeout);
121
- this.timeouts.delete(advertisementId);
122
- }
123
- this.logger.debug({ advertisementId }, 'advertisement hidden');
124
- }, animateHiding ? this.options.animationDelay || 0 : 0);
125
- this.timeouts.set(advertisementId, timeout);
126
- }
127
- this.store.delete(advertisementId);
128
- this.activeQueue.delete(advertisementId);
129
- this.waitingQueue.delete(advertisementId);
130
- this.tickWaitingQueue();
131
- this.logger.debug({ advertisementId }, 'advertisement hiding');
132
- return advertisement;
133
- };
134
- drain = () => {
135
- this.store.clear();
136
- this.timeouts.clear();
137
- this.waitingQueue.clear();
138
- this.activeQueue.clear();
139
- this.advertisementList.off();
140
- this.advertisementList.set(new Map());
141
- };
142
- }