@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.
@@ -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,6 @@
1
+ import { Storage } from '@streamlayer/sdk-web-storage';
2
+ export declare class AdvertisementStorage extends Storage {
3
+ constructor();
4
+ setShowed: (advertId: string) => void;
5
+ isShowed: (advertId: string) => string | undefined;
6
+ }
@@ -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
+ }
@@ -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?: 'list' | 'notification';
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
- const shortLink = await generateShortLink(transport, { web: desktopLink, mobile: mobileDeepLink });
38
- $store.set({
39
- data: shortLink.data?.link,
40
- loading: false,
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?: 'list' | 'notification';
8
+ openedFrom?: "list" | "notification";
9
9
  };
10
- } | undefined>, $feedList: ReturnType<GamificationBackground['feedList']['getStore']>) => {
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" | undefined;
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;
@@ -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['friends'], friendId: string, transport: Transport) => Promise<LeaderboardItem | undefined>;
5
+ export declare const friendSummary: ($eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification["friends"], friendId: string, transport: Transport) => Promise<LeaderboardItem | undefined>;
@@ -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?: 'list' | 'notification';
67
+ openedFrom?: "list" | "notification";
67
68
  }) => void | (() => void);
68
69
  closeQuestion: (questionId?: string) => void;
69
70
  openUser: (friendId: string) => Promise<void>;
@@ -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;
@@ -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['friends'], options?: LeaderboardOptions) => {
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;
@@ -10,10 +10,15 @@ export const $deepLink = (transport, params) => {
10
10
  if (!eventId) {
11
11
  return {};
12
12
  }
13
- const res = await client.generateDeepLink({
14
- data: { gamification: true, externalEventId: externalEventId, eventId: eventId },
15
- });
16
- return res.data?.attributes;
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,
@@ -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 | undefined) => AsyncIterable<SubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<VotingSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<VotingSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<QuestionSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<QuestionSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionResponse>)>;
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 | undefined) => AsyncIterable<SubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<VotingSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<VotingSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<QuestionSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<QuestionSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionResponse>)>;
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 | undefined) => AsyncIterable<SubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<VotingSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<VotingSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<QuestionSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<QuestionSubscriptionResponse>) | ((request: import("@bufbuild/protobuf").PartialMessage<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionRequest>, options?: import("@connectrpc/connect").CallOptions | undefined) => AsyncIterable<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedSubscriptionResponse>)>;
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';
@@ -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, {
@@ -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['friends'], transport: Transport) => {
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": "0.41.3",
3
+ "version": "1.1.0",
4
4
  "peerDependencies": {
5
- "@bufbuild/protobuf": "^1.8.0",
6
- "@fastify/deepmerge": "^1.3.0",
7
- "@streamlayer/sl-eslib": "^5.85.0",
8
- "nanostores": "^0.10.0",
9
- "@streamlayer/sdk-web-api": "^0.25.2",
10
- "@streamlayer/sdk-web-core": "^0.23.2",
11
- "@streamlayer/sdk-web-interfaces": "^0.22.2",
12
- "@streamlayer/sdk-web-logger": "^0.5.22",
13
- "@streamlayer/sdk-web-notifications": "^0.16.2",
14
- "@streamlayer/sdk-web-storage": "^0.4.9",
15
- "@streamlayer/sdk-web-types": "^0.23.4"
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.2"
18
+ "tslib": "^2.6.3"
19
19
  },
20
20
  "type": "module",
21
21
  "main": "./lib/index.js",