@streamlayer/feature-gamification 0.38.0 → 0.39.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.
@@ -27,7 +27,9 @@ export declare class GamificationBackground {
27
27
  /** opened question, using to download statistics */
28
28
  openedQuestionId: WritableAtom<{
29
29
  questionId: string;
30
- question?: FeedItem;
30
+ question?: FeedItem & {
31
+ openedFrom?: 'list' | 'notification';
32
+ };
31
33
  } | undefined>;
32
34
  /** opened question statistics */
33
35
  openedQuestion: ReturnType<typeof detail>;
@@ -44,6 +46,7 @@ export declare class GamificationBackground {
44
46
  questionSubscription?: ReturnType<typeof queries.questionSubscription>;
45
47
  private notifications;
46
48
  private log;
49
+ private transport;
47
50
  constructor(instance: StreamLayerContext);
48
51
  /**
49
52
  * Get id for notifications and link with current session
@@ -64,7 +67,9 @@ export declare class GamificationBackground {
64
67
  /**
65
68
  * Open question and mark notification for this question as viewed
66
69
  */
67
- openQuestion: (questionId: string, question?: FeedItem) => void;
70
+ openQuestion: (questionId: string, question?: FeedItem & {
71
+ openedFrom?: 'list' | 'notification';
72
+ }) => void;
68
73
  /**
69
74
  * Close question and mark notification for this question as viewed
70
75
  */
package/lib/background.js CHANGED
@@ -1,5 +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
4
  import '@streamlayer/sdk-web-core/store';
4
5
  import * as queries from './queries';
5
6
  import { detail } from './detail';
@@ -40,7 +41,9 @@ export class GamificationBackground {
40
41
  questionSubscription;
41
42
  notifications;
42
43
  log;
44
+ transport;
43
45
  constructor(instance) {
46
+ this.transport = instance.transport;
44
47
  this.log = createLogger('gamification-background');
45
48
  this.slStreamId = instance.stores.slStreamId.getAtomStore();
46
49
  this.organizationId = instance.stores.organizationSettings.getAtomStore();
@@ -58,9 +61,7 @@ export class GamificationBackground {
58
61
  if (item?.questionId) {
59
62
  this.questionSubscription = queries.questionSubscription(item.questionId, instance.transport);
60
63
  this.questionSubscription.addListener('feed-subscription-opened-question', (response) => {
61
- window.requestAnimationFrame(() => {
62
- this.openedQuestion.updateExtendedQuestion(response.data?.attributes?.question);
63
- });
64
+ this.openedQuestion.updateExtendedQuestion(response.data?.attributes?.question);
64
65
  });
65
66
  this.questionSubscription.connect();
66
67
  }
@@ -75,9 +76,19 @@ export class GamificationBackground {
75
76
  });
76
77
  this.feedSubscription = queries.feedSubscription(this.slStreamId, instance.transport);
77
78
  this.feedSubscription.addListener('feed-subscription-active-question', (response) => {
78
- const $activeQuestionId = this.activeQuestionId.getStore();
79
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
80
+ // @ts-ignore
81
+ const $activeQuestionId = this.activeQuestionId.store;
82
+ const activeQuestionId = $activeQuestionId.get().data?.question?.id;
83
+ const question = response.data?.attributes?.question;
84
+ if (!question) {
85
+ return;
86
+ }
87
+ // skip update question, avoid race condition
88
+ if (activeQuestionId && question.status === QuestionStatus.RESOLVED && question.id !== activeQuestionId) {
89
+ return;
90
+ }
79
91
  if ($activeQuestionId) {
80
- this.activeQuestionId;
81
92
  $activeQuestionId.mutate(response.data?.attributes);
82
93
  }
83
94
  });
@@ -121,6 +132,11 @@ export class GamificationBackground {
121
132
  };
122
133
  disconnect = () => {
123
134
  this.feedSubscription?.disconnect();
135
+ if (this.questionSubscription !== undefined) {
136
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
137
+ this.transport.removeSubscription(this.questionSubscription);
138
+ this.questionSubscription = undefined;
139
+ }
124
140
  };
125
141
  /**
126
142
  * Open question and mark notification for this question as viewed
@@ -0,0 +1,4 @@
1
+ export declare enum ERROR {
2
+ UNKNOWN = "unknown",
3
+ ALREADY_VOTED = "already_voted"
4
+ }
@@ -0,0 +1,5 @@
1
+ export var ERROR;
2
+ (function (ERROR) {
3
+ ERROR["UNKNOWN"] = "unknown";
4
+ ERROR["ALREADY_VOTED"] = "already_voted";
5
+ })(ERROR || (ERROR = {}));
package/lib/detail.d.ts CHANGED
@@ -2,17 +2,15 @@ import type { Transport } from '@streamlayer/sdk-web-api';
2
2
  import { FeedItem, ExtendedQuestion } from '@streamlayer/sdk-web-types';
3
3
  import { ReadableAtom } from 'nanostores';
4
4
  import { type GamificationBackground } from './background';
5
- type ExtendedQuestionStore = {
6
- data?: ExtendedQuestion;
7
- loading?: boolean;
8
- error?: string;
9
- };
10
5
  export declare const detail: (transport: Transport, $openedQuestionId: ReadableAtom<{
11
6
  questionId: string;
12
- question?: FeedItem;
7
+ question?: FeedItem & {
8
+ openedFrom?: 'list' | 'notification';
9
+ };
13
10
  } | undefined>, $feedList: ReturnType<GamificationBackground['feedList']['getStore']>) => {
14
- $store: import("nanostores").WritableAtom<import("@bufbuild/protobuf").PlainMessage<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedItem> | undefined>;
15
- $extendedStore: import("nanostores").MapStore<ExtendedQuestionStore>;
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;
13
+ }) | undefined>;
14
+ $extendedStore: import("@nanostores/query").FetcherStore<import("@bufbuild/protobuf").PlainMessage<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").ExtendedQuestion>, any>;
16
15
  updateExtendedQuestion: (question: ExtendedQuestion | undefined) => void;
17
16
  };
18
- export {};
package/lib/detail.js CHANGED
@@ -1,96 +1,48 @@
1
- import { createMapStore, createSingleStore } from '@streamlayer/sdk-web-interfaces';
2
- import { deepmerge } from '@fastify/deepmerge';
3
- import { onMount } from 'nanostores';
4
- import { getQuestionByUser } from './queries';
5
- const mergeArray = (options) => (target, source) => {
6
- let i = 0;
7
- const tl = target.length;
8
- const sl = source.length;
9
- const il = Math.max(tl, sl);
10
- const result = new Array(il);
11
- for (i = 0; i < il; ++i) {
12
- if (i < sl) {
13
- result[i] = options.deepmerge(target[i], source[i]);
14
- }
15
- else {
16
- result[i] = options.clone(target[i]);
17
- }
18
- }
19
- return result;
20
- };
21
- const mergeQuestion = deepmerge({ mergeArray });
1
+ import { batched } from 'nanostores';
2
+ import { $questionByUser } from './queries';
22
3
  export const detail = (transport, $openedQuestionId, $feedList) => {
23
- const $store = createSingleStore(undefined);
24
- onMount($store, () => {
25
- const cancel1 = $openedQuestionId.subscribe((openedQuestion) => {
26
- if (openedQuestion) {
27
- if (openedQuestion.question) {
28
- $store.set(openedQuestion.question);
29
- return;
30
- }
31
- const question = $feedList.get().data?.find((item) => item.id === openedQuestion.questionId);
32
- if (question) {
33
- $store.set(question);
34
- }
35
- else {
36
- console.error('Feed list is not loaded yet. Issue with the opened question.');
37
- }
38
- }
39
- else {
40
- $store.set(undefined);
41
- }
42
- });
43
- const cancel2 = $feedList.subscribe((feedList) => {
44
- const openedQuestion = $openedQuestionId.get();
45
- if (feedList.data && openedQuestion) {
46
- const question = $feedList.get().data?.find((item) => item.id === openedQuestion.questionId);
47
- if (question) {
48
- $store.set(question);
49
- }
50
- else {
51
- console.error('Feed list is not loaded yet. Issue with the opened question.');
52
- }
53
- }
54
- });
55
- return () => {
56
- cancel1();
57
- cancel2();
58
- };
59
- });
60
- const $extendedStore = createMapStore({
61
- data: undefined,
62
- loading: undefined,
63
- error: undefined,
64
- });
65
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
66
- $store.subscribe(async (item) => {
67
- if (item) {
68
- if (item.type === 'question') {
69
- $extendedStore.setKey('loading', true);
70
- const question = await getQuestionByUser(item.id, transport);
71
- $extendedStore.set({ data: question, loading: false });
72
- return;
73
- }
4
+ const $store = batched([$openedQuestionId, $feedList], () => {
5
+ const openedQuestion = $openedQuestionId.get();
6
+ if (!openedQuestion) {
7
+ return undefined;
8
+ }
9
+ const question = $feedList.get().data?.find((item) => item.id === openedQuestion?.questionId);
10
+ const openedFrom = openedQuestion?.question?.openedFrom;
11
+ if (question) {
12
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
13
+ // @ts-ignore
14
+ question.openedFrom = openedFrom;
15
+ return question;
74
16
  }
75
- $extendedStore.set({ data: undefined, loading: false });
17
+ return openedQuestion.question;
76
18
  });
19
+ const $storeQuestionId = batched($store, (item) => (item && item.type === 'question' ? item.id : undefined));
20
+ const $extendedStore = $questionByUser($storeQuestionId, transport);
77
21
  const updateExtendedQuestion = (question) => {
78
22
  const currentQuestion = $extendedStore.get().data;
79
- if (currentQuestion && question?.answers) {
80
- /**
81
- * We do not merge youVoted property, because it
82
- * can be overwritten by the subscription response,
83
- * which does not include user-specific data.
84
- */
85
- for (const answer of question.answers) {
86
- if (answer.youVoted !== true) {
87
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
88
- // @ts-ignore
89
- delete answer.youVoted;
90
- }
23
+ const mergeQuestionAnswers = (currentAnswers, newAnswers) => {
24
+ if (!currentAnswers || !newAnswers) {
25
+ return currentAnswers || newAnswers || [];
91
26
  }
92
- }
93
- $extendedStore.set({ data: mergeQuestion(currentQuestion, question) });
27
+ const answers = [];
28
+ for (let i = 0; i < currentAnswers.length; i++) {
29
+ answers.push({
30
+ ...currentAnswers[i],
31
+ ...newAnswers[i],
32
+ correct: currentAnswers[i].correct,
33
+ youVoted: currentAnswers[i].youVoted,
34
+ pointsEarned: currentAnswers[i].pointsEarned,
35
+ });
36
+ }
37
+ return answers;
38
+ };
39
+ $extendedStore.mutate({
40
+ ...question,
41
+ options: currentQuestion?.options,
42
+ subject: currentQuestion?.subject,
43
+ appearance: currentQuestion?.appearance,
44
+ answers: mergeQuestionAnswers(currentQuestion?.answers, question?.answers),
45
+ });
94
46
  };
95
47
  return { $store, $extendedStore, updateExtendedQuestion };
96
48
  };
@@ -10,6 +10,7 @@ import { deepLink } from './deepLink';
10
10
  import { OnboardingStatus } from './onboarding';
11
11
  import { LeaderboardItem } from './queries/leaderboard';
12
12
  import { GamificationBackground } from './background';
13
+ import { summary } from './userSummary';
13
14
  /**
14
15
  * Gamification (Games) Overlay
15
16
  * Includes:
@@ -21,7 +22,7 @@ import { GamificationBackground } from './background';
21
22
  */
22
23
  export declare class Gamification extends AbstractFeature<'games', PlainMessage<GamesOverlaySettings>> {
23
24
  /** user statistics (leaderboard panel) */
24
- userSummary: ApiStore<GetApiResponseType<typeof queries.$userSummary>>;
25
+ userSummary: ReturnType<typeof summary>;
25
26
  /** feed list (pick history) */
26
27
  feedList: ApiStore<GetApiResponseType<typeof queries.$feedList>>;
27
28
  /** friends list */
@@ -44,20 +45,25 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
44
45
  openedUser: WritableAtom<LeaderboardItem | undefined>;
45
46
  closeFeature: () => void;
46
47
  openFeature: () => void;
48
+ feedSubscription: GamificationBackground['feedSubscription'];
49
+ activeQuestionId: GamificationBackground['activeQuestionId'];
50
+ openedQuestionId: GamificationBackground['openedQuestionId'];
47
51
  private notifications;
48
52
  private transport;
49
53
  /** gamification background class, handle subscriptions and notifications for closed overlay */
50
54
  private background;
51
55
  /** Browser cache */
52
56
  private storage;
57
+ private submitAnswerTimeout;
53
58
  constructor(config: FeatureProps, source: FeatureSource, instance: StreamLayerContext);
54
59
  get isInteractiveAllowed(): boolean;
55
60
  checkInteractiveFlag: () => void;
56
- connect: (transport: StreamLayerContext['transport']) => void;
61
+ connect: () => void;
57
62
  disconnect: () => void;
58
63
  submitAnswer: (questionId: string, answerId: string) => Promise<void>;
59
- skipQuestion: (questionId: string) => Promise<void>;
60
- openQuestion: (questionId: string, question?: FeedItem) => void;
64
+ openQuestion: (questionId: string, question?: FeedItem & {
65
+ openedFrom?: 'list' | 'notification';
66
+ }) => void;
61
67
  closeQuestion: (questionId?: string) => void;
62
68
  openUser: (userId: string) => void;
63
69
  closeUser: () => void;
@@ -1,5 +1,6 @@
1
- import { AbstractFeature, ApiStore, FeatureStatus, SingleStore, createSingleStore, } from '@streamlayer/sdk-web-interfaces';
2
- import { QuestionStatus, QuestionType, FeatureType, SilenceSetting, } from '@streamlayer/sdk-web-types';
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ import { AbstractFeature, ApiStore, SingleStore, createSingleStore, eventBus, } from '@streamlayer/sdk-web-interfaces';
3
+ import { QuestionStatus, QuestionType, FeatureType, SilenceSetting, PickHistoryStatus, } from '@streamlayer/sdk-web-types';
3
4
  import { NotificationType } from '@streamlayer/sdk-web-notifications';
4
5
  import '@streamlayer/sdk-web-core/store';
5
6
  import * as queries from './queries';
@@ -9,6 +10,9 @@ import { leaderboard } from './leaderboard';
9
10
  import { deepLink } from './deepLink';
10
11
  import { OnboardingStatus, onboarding } from './onboarding';
11
12
  import { GamificationBackground, InteractiveAllowed } from './background';
13
+ import { ERROR } from './constants';
14
+ import { $questionByUser } from './queries';
15
+ import { summary } from './userSummary';
12
16
  const InteractiveQuestionTypes = new Set([QuestionType.POLL, QuestionType.PREDICTION, QuestionType.TRIVIA]);
13
17
  /**
14
18
  * Gamification (Games) Overlay
@@ -41,17 +45,23 @@ export class Gamification extends AbstractFeature {
41
45
  openedUser;
42
46
  closeFeature;
43
47
  openFeature;
48
+ feedSubscription;
49
+ activeQuestionId;
50
+ openedQuestionId;
44
51
  notifications;
45
52
  transport;
46
53
  /** gamification background class, handle subscriptions and notifications for closed overlay */
47
54
  background;
48
55
  /** Browser cache */
49
56
  storage;
57
+ submitAnswerTimeout;
50
58
  constructor(config, source, instance) {
51
59
  super(config, source);
52
60
  this.background = new GamificationBackground(instance);
61
+ this.feedSubscription = this.background.feedSubscription;
62
+ this.activeQuestionId = this.background.activeQuestionId;
63
+ this.openedQuestionId = this.background.openedQuestionId;
53
64
  this.storage = new GamificationStorage();
54
- this.userSummary = new ApiStore(queries.$userSummary(this.background.slStreamId, this.background.userId, instance.transport), 'gamification:userSummary');
55
65
  this.feedList = this.background.feedList;
56
66
  this.friends = new ApiStore(queries.$friends(this.background.userId, instance.transport), 'gamification:friends');
57
67
  this.currentUserId = this.background.userId;
@@ -64,21 +74,13 @@ export class Gamification extends AbstractFeature {
64
74
  this.openFeature = () => instance.sdk.openFeature(FeatureType.GAMES);
65
75
  this.openedQuestion = this.background.openedQuestion;
66
76
  this.deepLink = deepLink(this.transport, this.background.slStreamId, instance.stores.providerStreamId.getStore(), this.background.userId);
77
+ this.userSummary = summary(this.background.slStreamId, this.background.userId, this.friends, this.transport);
67
78
  this.leaderboardList = leaderboard(this.transport, this.background.slStreamId, this.background.userId, this.friends);
68
- this.status.subscribe((status) => {
69
- if (status === FeatureStatus.Ready) {
70
- this.connect(instance.transport);
71
- }
72
- else {
73
- this.disconnect();
74
- }
75
- });
79
+ this.connect();
76
80
  // refresh leaderboard on user summary update after earning points
77
- this.userSummary.listen((userSummary) => {
78
- if (this.leaderboardList.$store.lc !== 0 && userSummary?.data?.summary) {
79
- window.requestAnimationFrame(() => {
80
- this.leaderboardList.invalidate();
81
- });
81
+ this.userSummary.$store.listen((userSummary) => {
82
+ if (this.leaderboardList.$store.lc !== 0 && userSummary?.summary) {
83
+ this.leaderboardList.invalidate(); // verified, it's necessary
82
84
  }
83
85
  });
84
86
  /**
@@ -103,41 +105,79 @@ export class Gamification extends AbstractFeature {
103
105
  const allowed = !onboardingEnabled || onboardingCompleted || optInEnabled !== true;
104
106
  this.background.interactiveAllowed.set(allowed ? InteractiveAllowed.ALLOWED : InteractiveAllowed.DISALLOWED);
105
107
  };
106
- connect = (transport) => {
107
- this.userSummary.invalidate();
108
- this.leaderboardList.invalidate();
109
- this.feedList.invalidate();
110
- this.friends.invalidate();
111
- this.background.feedSubscription.addListener('feed-subscription-prediction-close', (response) => {
108
+ connect = () => {
109
+ this.background.feedSubscription.addListener('feed-subscription-prediction-close', async (response) => {
112
110
  if (!this.isInteractiveAllowed) {
113
111
  return;
114
112
  }
115
- window.requestAnimationFrame(async () => {
116
- const question = response.data?.attributes?.question;
117
- const feedItem = response.data?.attributes?.feedItem;
118
- if (!question) {
119
- return;
113
+ const question = response.data?.attributes?.question;
114
+ const feedItem = response.data?.attributes?.feedItem;
115
+ if (!question || !feedItem?.attributes) {
116
+ return;
117
+ }
118
+ const { status, type, id, answers } = question;
119
+ if (status === QuestionStatus.RESOLVED && type === QuestionType.PREDICTION) {
120
+ const notificationId = this.background.getCurrentSessionId({
121
+ prefix: `notification-id:${id}`,
122
+ });
123
+ const feedList = [...(this.feedList.getValues().data || [])];
124
+ const questionFromFeedListIndex = feedList.findIndex((item) => item.id === id);
125
+ const questionFromFeedList = feedList[questionFromFeedListIndex];
126
+ // @ts-ignore
127
+ let votedAnswerId = questionFromFeedList?.attributes?.attributes?.value?.answerId;
128
+ // get voted answer id from extended question or feed list
129
+ const data = $questionByUser(id, this.transport);
130
+ // order of operations is important here
131
+ const cancel = data.subscribe(() => { });
132
+ await data.get().promise;
133
+ const extendedQuestion = data.get().data;
134
+ cancel();
135
+ window.requestAnimationFrame(() => {
136
+ data.invalidate();
137
+ });
138
+ // get extended question data and mark as dirty
139
+ if (!votedAnswerId) {
140
+ votedAnswerId = extendedQuestion?.answers.find(({ youVoted }) => youVoted)?.id;
120
141
  }
121
- const { status, type, id } = question;
122
- if (status === QuestionStatus.RESOLVED && type === QuestionType.PREDICTION) {
123
- const notificationId = this.background.getCurrentSessionId({
124
- prefix: `notification-id:${id}`,
125
- });
126
- const question = await queries.getQuestionByUser(id, transport);
127
- const correctAnswer = question?.answers.find(({ correct }) => correct);
128
- const votedAnswer = question?.answers.find(({ youVoted }) => youVoted);
129
- if (!votedAnswer || !question)
130
- return;
142
+ const correctAnswer = answers.find(({ correct }) => correct);
143
+ const votedAnswer = votedAnswerId ? answers.find(({ id }) => id === votedAnswerId) : undefined;
144
+ const votedCorrect = !!votedAnswer?.correct;
145
+ // update question in feed list if it's there
146
+ if (questionFromFeedList) {
147
+ if (feedList[questionFromFeedListIndex]?.attributes?.attributes.case === 'question') {
148
+ try {
149
+ // @ts-ignore
150
+ feedList[questionFromFeedListIndex].attributes.attributes.value.answerId = votedAnswerId;
151
+ // @ts-ignore
152
+ feedList[questionFromFeedListIndex].attributes.attributes.value.openForVoting = false;
153
+ if (votedAnswerId) {
154
+ // @ts-ignore
155
+ feedList[questionFromFeedListIndex].attributes.attributes.value.status = votedCorrect
156
+ ? PickHistoryStatus.WON
157
+ : PickHistoryStatus.LOST;
158
+ }
159
+ // eslint-disable-next-line no-empty
160
+ }
161
+ catch (e) { }
162
+ this.feedList.getStore().mutate(feedList);
163
+ }
164
+ }
165
+ if (!votedAnswer || !correctAnswer)
166
+ return;
167
+ // avoid showing notification if question already opened
168
+ if (this.openedQuestionId.get()?.questionId !== question.id) {
131
169
  this.notifications.add({
132
170
  type: NotificationType.QUESTION_RESOLVED,
133
171
  action: () => this.openQuestion(question.id, feedItem),
134
172
  close: () => this.closeQuestion(id),
135
- autoHideDuration: correctAnswer?.youVoted ? 15000 : 12000,
173
+ autoHideDuration: votedCorrect ? 15000 : 12000,
136
174
  id: notificationId,
175
+ emitEvent: false,
137
176
  data: {
177
+ questionId: id,
138
178
  questionType: QuestionType.PREDICTION,
139
179
  question: {
140
- title: correctAnswer?.youVoted
180
+ title: votedCorrect
141
181
  ? `Congratulations! You answered correctly! You won ${correctAnswer.points} pts!`
142
182
  : `Better luck next time! Correct: ${correctAnswer?.text}!`,
143
183
  votedAnswer: {
@@ -145,41 +185,183 @@ export class Gamification extends AbstractFeature {
145
185
  points: votedAnswer?.points,
146
186
  },
147
187
  correctAnswerTitle: correctAnswer?.text,
148
- correct: correctAnswer?.youVoted,
188
+ correct: !!votedCorrect,
149
189
  predictionResult: status === QuestionStatus.RESOLVED,
150
190
  questionTitle: question?.subject,
151
191
  },
152
192
  },
153
193
  });
154
- this.userSummary.invalidate();
155
194
  }
156
- });
195
+ this.userSummary.invalidate(); // verified, it's necessary
196
+ }
157
197
  });
158
- this.background.feedSubscription.addListener('feed-subscription-questions-list', () => {
159
- window.requestAnimationFrame(() => {
160
- this.feedList?.invalidate();
161
- });
198
+ // update feed list on question update received from subscription
199
+ // add new question to the top of the list
200
+ this.background.feedSubscription.addListener('feed-subscription-questions-list', (response) => {
201
+ const feedList = [...(this.feedList.getStore().value?.data || [])];
202
+ const feedItem = response.data?.attributes?.feedItem;
203
+ const questionIndex = feedList.findIndex((item) => item.id === feedItem?.id);
204
+ if (!feedItem) {
205
+ return;
206
+ }
207
+ if (questionIndex !== -1) {
208
+ if (feedItem.attributes?.attributes.case === 'question' &&
209
+ feedList[questionIndex].attributes?.attributes.case === 'question') {
210
+ const prev = feedList[questionIndex];
211
+ if (prev.attributes) {
212
+ feedList[questionIndex] = {
213
+ ...feedList[questionIndex],
214
+ attributes: {
215
+ ...prev.attributes,
216
+ attributes: {
217
+ ...prev.attributes.attributes,
218
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
219
+ // @ts-ignore
220
+ value: {
221
+ ...prev.attributes.attributes.value,
222
+ ...feedItem.attributes.attributes.value,
223
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
224
+ // @ts-ignore
225
+ answerId: prev.attributes.attributes.value.answerId,
226
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
227
+ // @ts-ignore
228
+ status: prev.attributes.attributes.value.status,
229
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
230
+ // @ts-ignore
231
+ openForVoting: prev.attributes.attributes.value.openForVoting,
232
+ },
233
+ },
234
+ },
235
+ };
236
+ }
237
+ }
238
+ else {
239
+ const prev = feedList[questionIndex];
240
+ if (prev.attributes && feedItem.attributes) {
241
+ feedList[questionIndex] = {
242
+ ...feedList[questionIndex],
243
+ attributes: {
244
+ ...prev.attributes,
245
+ attributes: {
246
+ ...prev.attributes.attributes,
247
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
248
+ // @ts-ignore
249
+ value: {
250
+ ...prev.attributes.attributes.value,
251
+ ...feedItem.attributes.attributes.value,
252
+ },
253
+ },
254
+ },
255
+ };
256
+ }
257
+ }
258
+ }
259
+ if (questionIndex === -1) {
260
+ feedList.unshift(feedItem);
261
+ eventBus.emit('poll', {
262
+ action: 'received',
263
+ payload: {
264
+ questionId: feedItem.id,
265
+ questionType: feedItem.attributes?.type,
266
+ },
267
+ });
268
+ }
269
+ this.feedList.getStore().mutate(feedList);
162
270
  });
163
271
  };
272
+ // not used
164
273
  disconnect = () => {
274
+ this.background.feedSubscription.removeListener('feed-subscription-prediction-close');
165
275
  this.background.feedSubscription.removeListener('feed-subscription-questions-list');
166
276
  };
167
277
  submitAnswer = async (questionId, answerId) => {
168
- await actions.submitAnswer(this.transport, { questionId, answerId });
169
- // Todo: add invalidate openedQuestion
170
- this.feedList.invalidate();
171
- this.userSummary.invalidate();
172
- };
173
- skipQuestion = async (questionId) => {
174
- await actions.skipQuestion(this.transport, questionId);
175
- this.feedList.invalidate();
176
- this.userSummary.invalidate();
278
+ const data = $questionByUser(questionId, this.transport);
279
+ const updateQuestionAndFieldList = () => {
280
+ const feedList = this.feedList.getValues().data;
281
+ if (!feedList) {
282
+ return;
283
+ }
284
+ const questionIndex = feedList.findIndex((item) => item.id === questionId);
285
+ const poll = feedList[questionIndex];
286
+ const question = poll?.attributes?.attributes.case === 'question' && poll.attributes.attributes.value;
287
+ if (question) {
288
+ eventBus.emit('poll', {
289
+ action: 'voted',
290
+ payload: {
291
+ questionId,
292
+ questionType: question.questionType,
293
+ },
294
+ });
295
+ const cancel = data.subscribe(() => { });
296
+ const extendedQuestion = data.get().data;
297
+ cancel();
298
+ if (extendedQuestion) {
299
+ const correctAnswer = extendedQuestion.answers.find((answer) => answer.correct === true);
300
+ const votedAnswerIdx = extendedQuestion.answers.findIndex((answer) => answer.id === answerId);
301
+ const votedAnswer = extendedQuestion.answers[votedAnswerIdx];
302
+ // @ts-ignore
303
+ feedList[questionIndex].attributes.attributes.value.answerId = answerId;
304
+ // @ts-ignore
305
+ feedList[questionIndex].attributes.attributes.value.openForVoting = false;
306
+ // @ts-ignore
307
+ feedList[questionIndex].attributes.attributes.value.text = votedAnswer?.text || '';
308
+ if (correctAnswer) {
309
+ // @ts-ignore
310
+ feedList[questionIndex].attributes.attributes.value.status =
311
+ correctAnswer.id === answerId ? PickHistoryStatus.WON : PickHistoryStatus.LOST;
312
+ }
313
+ this.feedList.getStore().mutate([...feedList]);
314
+ extendedQuestion.answers[votedAnswerIdx].correct = correctAnswer?.id === answerId;
315
+ extendedQuestion.answers[votedAnswerIdx].youVoted = true;
316
+ extendedQuestion.answers.forEach((answer) => {
317
+ answer.percentageDecimal = 0;
318
+ });
319
+ if (correctAnswer?.id === answerId) {
320
+ extendedQuestion.answers[votedAnswerIdx].pointsEarned =
321
+ extendedQuestion.status === QuestionStatus.RESOLVED ? 0 : correctAnswer.points;
322
+ }
323
+ data.mutate({ ...extendedQuestion });
324
+ }
325
+ if (this.submitAnswerTimeout) {
326
+ clearTimeout(this.submitAnswerTimeout);
327
+ }
328
+ this.submitAnswerTimeout = setTimeout(() => {
329
+ this.userSummary.invalidate(); // verified, it's necessary
330
+ }, 1000);
331
+ }
332
+ };
333
+ try {
334
+ await actions.submitAnswer(this.transport, { questionId, answerId });
335
+ updateQuestionAndFieldList();
336
+ }
337
+ catch (error) {
338
+ if (error.message === ERROR.ALREADY_VOTED) {
339
+ this.userSummary.invalidate();
340
+ const cancel = data.subscribe(() => { });
341
+ data.invalidate();
342
+ cancel();
343
+ }
344
+ throw error;
345
+ }
177
346
  };
178
347
  openQuestion = (questionId, question) => {
179
348
  this.notifications.close(this.background.getCurrentSessionId({
180
349
  prefix: 'notification',
181
350
  entity: questionId,
182
351
  }));
352
+ let questionType = question?.attributes?.type;
353
+ if (!questionType) {
354
+ const feedList = this.feedList.getStore().value?.data || [];
355
+ questionType = feedList.find((item) => item.id === questionId)?.attributes?.type;
356
+ }
357
+ eventBus.emit('poll', {
358
+ action: 'opened',
359
+ payload: {
360
+ questionId,
361
+ questionType,
362
+ questionOpenedFrom: question?.openedFrom,
363
+ },
364
+ });
183
365
  return this.background.openQuestion(questionId, question);
184
366
  };
185
367
  closeQuestion = (questionId) => {
@@ -217,7 +399,9 @@ export class Gamification extends AbstractFeature {
217
399
  prefix: 'notification',
218
400
  entity: question.data.question.id,
219
401
  }),
402
+ emitEvent: true,
220
403
  data: {
404
+ questionId: question.data.question.id,
221
405
  questionType: question.data.question.type,
222
406
  question: {
223
407
  title: question.data.question.notification.title,
@@ -231,7 +415,7 @@ export class Gamification extends AbstractFeature {
231
415
  const instantView = {
232
416
  heading: question.data.question.notification.title,
233
417
  body: question.data.question.notification.body,
234
- imageMode: optionsValue.imageMode,
418
+ imageMode: optionsValue?.imageMode,
235
419
  image: optionsValue?.image,
236
420
  video: {
237
421
  id: optionsValue?.video?.id || '',
@@ -248,8 +432,10 @@ export class Gamification extends AbstractFeature {
248
432
  action: () => question.data?.question && this.openQuestion(question.data.question.id, question.data.feedItem),
249
433
  close: () => question.data?.question && this.closeQuestion(question.data.question.id),
250
434
  autoHideDuration: 1000 * 120,
435
+ emitEvent: true,
251
436
  id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
252
437
  data: {
438
+ questionId: question.data.question.id,
253
439
  questionType: question.data.question.type,
254
440
  insight: instantView,
255
441
  },
@@ -271,8 +457,10 @@ export class Gamification extends AbstractFeature {
271
457
  action: () => question.data?.question && this.openQuestion(question.data.question.id, question.data.feedItem),
272
458
  close: () => question.data?.question && this.closeQuestion(question.data.question.id),
273
459
  autoHideDuration: 1000 * 120,
460
+ emitEvent: true,
274
461
  id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
275
462
  data: {
463
+ questionId: question.data.question.id,
276
464
  questionType: question.data.question.type,
277
465
  tweet: tweetView,
278
466
  },
@@ -34,19 +34,17 @@ export const leaderboard = (transport, $eventId, $userId, $friends, options) =>
34
34
  const request = {
35
35
  eventId: eventId,
36
36
  usersIds: friendsIds,
37
- pagination: { page: 0, pageSize: options?.pageSize || defaultOptions.pageSize },
38
37
  };
39
38
  const newData = await fetch(request);
40
39
  $store.set({
41
40
  data: newData.data.map((item) => item.attributes),
42
- hasMore: true,
41
+ hasMore: false,
43
42
  key: Date.now(),
44
43
  loading: false,
45
44
  });
46
45
  if (newData.meta) {
47
46
  maxPage = Math.round(newData.meta.count / newData.meta.pageSize);
48
47
  }
49
- $pagination.set(request.pagination);
50
48
  }
51
49
  };
52
50
  const invalidate = () => {
@@ -65,33 +63,32 @@ export const leaderboard = (transport, $eventId, $userId, $friends, options) =>
65
63
  onMount($store, () => {
66
64
  const cancelRefetchListener = $eventId.listen(refetch);
67
65
  const cancelRefetchByFriendsListener = $friends.listen(refetch);
68
- const cancelPaginationListener = $pagination.listen(async (pagination) => {
69
- const eventId = $eventId.get();
70
- if (pagination.page > 0 && eventId) {
71
- if (pagination.page < maxPage) {
72
- $store.setKey('loading', true);
73
- const request = {
74
- eventId: eventId,
75
- pagination,
76
- };
77
- const newData = await fetch(request);
78
- const prevData = $store.get().data || [];
79
- $store.set({
80
- data: [...prevData, ...newData.data.map((item) => item.attributes)],
81
- key: $store.get().key,
82
- loading: false,
83
- hasMore: true,
84
- });
85
- }
86
- else {
87
- $store.setKey('hasMore', false);
88
- }
89
- }
90
- });
66
+ // const cancelPaginationListener = $pagination.listen(async (pagination) => {
67
+ // const eventId = $eventId.get()
68
+ // if (pagination.page > 0 && eventId) {
69
+ // if (pagination.page < maxPage) {
70
+ // $store.setKey('loading', true)
71
+ // const request = {
72
+ // eventId: eventId as unknown as bigint,
73
+ // pagination,
74
+ // }
75
+ // const newData = await fetch(request)
76
+ // const prevData = $store.get().data || []
77
+ // $store.set({
78
+ // data: [...prevData, ...newData.data.map((item) => item.attributes as LeaderboardItem)],
79
+ // key: $store.get().key,
80
+ // loading: false,
81
+ // hasMore: true,
82
+ // })
83
+ // } else {
84
+ // $store.setKey('hasMore', false)
85
+ // }
86
+ // }
87
+ // })
91
88
  return () => {
92
89
  cancelRefetchListener();
93
90
  cancelRefetchByFriendsListener();
94
- cancelPaginationListener();
91
+ // cancelPaginationListener()
95
92
  };
96
93
  });
97
94
  return { $store, fetchMore, invalidate };
package/lib/onboarding.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createSingleStore } from '@streamlayer/sdk-web-interfaces';
1
+ import { createSingleStore, eventBus } from '@streamlayer/sdk-web-interfaces';
2
2
  import { NotificationType } from '@streamlayer/sdk-web-notifications';
3
3
  import { QuestionType } from '@streamlayer/sdk-web-types';
4
4
  import { GamificationStorage } from './storage';
@@ -31,6 +31,7 @@ const showOnboardingInApp = (service, background, notifications, storage) => {
31
31
  persistent: true,
32
32
  autoHideDuration: 1000000,
33
33
  data: {
34
+ questionId: 'onboarding',
34
35
  questionType: QuestionType.UNSET,
35
36
  onboarding: {
36
37
  ...inplayGame,
@@ -112,7 +113,7 @@ export const onboarding = (service, background, transport, notifications) => {
112
113
  }
113
114
  }
114
115
  if (onboardingStatus === OnboardingStatus.Completed) {
115
- background.activeQuestionId.invalidate();
116
+ background.activeQuestionId.invalidate(); // verified, it's necessary
116
117
  }
117
118
  storage.setOnboardingInstantOpen({
118
119
  userId: background.userId.get() || '',
@@ -149,6 +150,10 @@ export const onboarding = (service, background, transport, notifications) => {
149
150
  userId: background.userId.get() || '',
150
151
  eventId,
151
152
  }, OnboardingStatus.Completed);
153
+ eventBus.emit('poll', {
154
+ action: 'onboardingPassed',
155
+ payload: {},
156
+ });
152
157
  const notificationId = background.getCurrentSessionId({ prefix: 'onboarding' });
153
158
  notifications.close(notificationId);
154
159
  }
@@ -1,8 +1,19 @@
1
+ import { ConnectError, Code } from '@connectrpc/connect';
1
2
  import { Feed } from '@streamlayer/sl-eslib/interactive/feed/interactive.feed_connect';
2
- export const submitAnswer = (transport, data) => {
3
+ import { ERROR } from '../constants';
4
+ export const submitAnswer = async (transport, data) => {
3
5
  const { client, createRequestOptions } = transport.createPromiseClient(Feed, { method: 'submitAnswer' });
4
6
  const contextValues = createRequestOptions({ retryAttempts: 0 });
5
- return client.submitAnswer({ data }, { contextValues });
7
+ try {
8
+ return await client.submitAnswer({ data }, { contextValues });
9
+ }
10
+ catch (error) {
11
+ const cErr = ConnectError.from(error);
12
+ if (cErr?.code === Code.AlreadyExists) {
13
+ throw new Error(ERROR.ALREADY_VOTED);
14
+ }
15
+ throw new Error(ERROR.UNKNOWN);
16
+ }
6
17
  };
7
18
  export const submitInplay = (transport, eventId) => {
8
19
  const { client, createRequestOptions } = transport.createPromiseClient(Feed, { method: 'submitInplay' });
@@ -15,6 +15,8 @@ export const $deepLink = (transport, params) => {
15
15
  });
16
16
  return res.data?.attributes;
17
17
  },
18
+ dedupeTime: 1000 * 60 * 60 * 24, // 24 hours
19
+ refetchInterval: 0,
18
20
  });
19
21
  };
20
22
  export const generateShortLink = (transport, { web, mobile }) => {
@@ -329,7 +329,7 @@ export declare const questionSubscription: (questionId: string, transport: Trans
329
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>)>;
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
- export declare const $questionByUser: ($questionId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").ExtendedQuestion | undefined, any>;
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
333
  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
334
  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
335
  export { $userSummary, $leaderboardList } from './leaderboard';
@@ -16,6 +16,8 @@ export const $activeQuestion = (slStreamId, transport) => {
16
16
  });
17
17
  return res.data?.attributes;
18
18
  },
19
+ dedupeTime: 1000 * 60 * 10, // 10 minutes
20
+ refetchInterval: 0,
19
21
  });
20
22
  };
21
23
  export const feedSubscription = ($slStreamId, transport) => {
@@ -60,6 +62,7 @@ export const $questionByUser = ($questionId, transport) => {
60
62
  });
61
63
  return res.data?.attributes?.question;
62
64
  },
65
+ dedupeTime: 1000 * 60 * 5,
63
66
  });
64
67
  };
65
68
  export const $pickHistory = (slStreamId, transport) => {
@@ -97,6 +100,8 @@ export const $feedList = ($slStreamId, $interactiveAllowed, transport) => {
97
100
  });
98
101
  return res.data;
99
102
  },
103
+ dedupeTime: 0,
104
+ refetchInterval: 0,
100
105
  });
101
106
  };
102
107
  export { $userSummary, $leaderboardList } from './leaderboard';
@@ -1,8 +1,9 @@
1
1
  import type { Transport } from '@streamlayer/sdk-web-api';
2
2
  import { ReadableAtom } from 'nanostores';
3
- import { ListRequest } from '@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb';
3
+ import { ListRequest, SummaryRequest } from '@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb';
4
4
  import { PartialMessage } from '@bufbuild/protobuf';
5
5
  export { LeaderboardItem } from '@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb';
6
6
  export declare const $userSummary: ($eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb").LeaderboardSummaryItem | undefined, any>;
7
7
  export declare const $leaderboardList: ($eventId: ReadableAtom<string | undefined>, _: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb").ListResponse_ListResponseData[], any>;
8
8
  export declare const createLeaderboardListFetch: (transport: Transport) => (params: PartialMessage<ListRequest>) => Promise<import("@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb").ListResponse>;
9
+ export declare const createUserSummaryFetch: (transport: Transport) => (params: PartialMessage<SummaryRequest>) => Promise<import("@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb").SummaryResponse>;
@@ -34,3 +34,7 @@ export const createLeaderboardListFetch = (transport) => {
34
34
  const { client } = transport.createPromiseClient(Leaderboard, { method: 'list' });
35
35
  return (params) => client.list(params);
36
36
  };
37
+ export const createUserSummaryFetch = (transport) => {
38
+ const { client } = transport.createPromiseClient(Leaderboard, { method: 'summary' });
39
+ return (params) => client.summary(params);
40
+ };
@@ -8,5 +8,7 @@ export const $moderation = (slStreamId, transport) => {
8
8
  });
9
9
  return res.data?.attributes;
10
10
  },
11
+ refetchInterval: 0,
12
+ dedupeTime: 1000 * 60 * 60,
11
13
  });
12
14
  };
@@ -0,0 +1,8 @@
1
+ import type { Transport } from '@streamlayer/sdk-web-api';
2
+ import { ReadableAtom } from 'nanostores';
3
+ import { LeaderboardSummaryItem } from '@streamlayer/sl-eslib/interactive/leaderboard/interactive.leaderboard_pb';
4
+ import { Gamification } from '.';
5
+ export declare const summary: ($eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification['friends'], transport: Transport) => {
6
+ $store: import("nanostores").MapStore<LeaderboardSummaryItem | undefined>;
7
+ invalidate: () => void;
8
+ };
@@ -0,0 +1,38 @@
1
+ import { createMapStore } from '@streamlayer/sdk-web-interfaces';
2
+ import { onMount } from 'nanostores';
3
+ import { createUserSummaryFetch } from './queries/leaderboard';
4
+ export const summary = ($eventId, $userId, $friends, transport) => {
5
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
6
+ // @ts-ignore
7
+ const $store = createMapStore(undefined);
8
+ const fetch = createUserSummaryFetch(transport);
9
+ const refetch = async () => {
10
+ const eventId = $eventId.get();
11
+ const userId = $userId.get();
12
+ const usersIds = $friends
13
+ .getStore()
14
+ .get()
15
+ .data?.map((friend) => friend.slId) || [];
16
+ const request = {
17
+ eventId: eventId,
18
+ userId: userId,
19
+ usersIds: [...usersIds, userId],
20
+ };
21
+ const res = await fetch(request);
22
+ $store.set(res.data?.attributes);
23
+ };
24
+ const invalidate = () => {
25
+ void refetch();
26
+ };
27
+ onMount($store, () => {
28
+ const cancelRefetchListener = $eventId.listen(refetch);
29
+ const cancelRefetchByFriendsListener = $friends.listen(refetch);
30
+ const cancelRefetchByUserListener = $userId.listen(refetch);
31
+ return () => {
32
+ cancelRefetchListener();
33
+ cancelRefetchByFriendsListener();
34
+ cancelRefetchByUserListener();
35
+ };
36
+ });
37
+ return { $store, invalidate };
38
+ };
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@streamlayer/feature-gamification",
3
- "version": "0.38.0",
3
+ "version": "0.39.1",
4
4
  "peerDependencies": {
5
5
  "@bufbuild/protobuf": "^1.7.2",
6
- "@fastify/deepmerge": "*",
7
- "@streamlayer/sl-eslib": "^5.79.3",
8
- "nanostores": "^0.9.5",
9
- "@streamlayer/sdk-web-api": "^0.23.0",
10
- "@streamlayer/sdk-web-core": "^0.21.3",
11
- "@streamlayer/sdk-web-interfaces": "^0.20.6",
12
- "@streamlayer/sdk-web-logger": "^0.5.17",
13
- "@streamlayer/sdk-web-notifications": "^0.14.2",
14
- "@streamlayer/sdk-web-storage": "^0.4.4",
15
- "@streamlayer/sdk-web-types": "^0.22.4"
6
+ "@fastify/deepmerge": "^1.3.0",
7
+ "@streamlayer/sl-eslib": "^5.83.1",
8
+ "nanostores": "^0.10.0",
9
+ "@streamlayer/sdk-web-api": "^0.24.1",
10
+ "@streamlayer/sdk-web-core": "^0.22.1",
11
+ "@streamlayer/sdk-web-interfaces": "^0.21.0",
12
+ "@streamlayer/sdk-web-logger": "^0.5.18",
13
+ "@streamlayer/sdk-web-notifications": "^0.15.0",
14
+ "@streamlayer/sdk-web-storage": "^0.4.5",
15
+ "@streamlayer/sdk-web-types": "^0.23.0"
16
16
  },
17
17
  "devDependencies": {
18
18
  "tslib": "^2.6.2"