@streamlayer/feature-gamification 0.34.0 → 0.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/background.d.ts +9 -0
- package/lib/background.js +22 -1
- package/lib/gamification.d.ts +12 -1
- package/lib/gamification.js +121 -85
- package/lib/leaderboard.d.ts +4 -2
- package/lib/leaderboard.js +26 -5
- package/lib/onboarding.js +6 -4
- package/lib/queries/friends.d.ts +3 -0
- package/lib/queries/friends.js +16 -0
- package/lib/queries/index.d.ts +3 -2
- package/lib/queries/index.js +14 -20
- package/package.json +8 -8
package/lib/background.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ import '@streamlayer/sdk-web-core/store';
|
|
|
4
4
|
import { ReadableAtom, WritableAtom } from 'nanostores';
|
|
5
5
|
import * as queries from './queries';
|
|
6
6
|
import { detail } from './detail';
|
|
7
|
+
export declare enum InteractiveAllowed {
|
|
8
|
+
ALLOWED = "allowed",
|
|
9
|
+
DISALLOWED = "disallowed"
|
|
10
|
+
}
|
|
7
11
|
/**
|
|
8
12
|
* Background class for Gamification feature
|
|
9
13
|
*/
|
|
@@ -14,6 +18,11 @@ export declare class GamificationBackground {
|
|
|
14
18
|
organizationId: ReadableAtom<string | undefined>;
|
|
15
19
|
/** current user id */
|
|
16
20
|
userId: ReadableAtom<string | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* flag to allow interactive questions (polls, trivia, predictions)
|
|
23
|
+
* controlled by opt-in and onboarding flags
|
|
24
|
+
*/
|
|
25
|
+
interactiveAllowed: WritableAtom<InteractiveAllowed>;
|
|
17
26
|
/** opened question, using to download statistics */
|
|
18
27
|
openedQuestionId: WritableAtom<string | undefined>;
|
|
19
28
|
/** opened question statistics */
|
package/lib/background.js
CHANGED
|
@@ -3,6 +3,11 @@ import { createLogger } from '@streamlayer/sdk-web-logger';
|
|
|
3
3
|
import '@streamlayer/sdk-web-core/store';
|
|
4
4
|
import * as queries from './queries';
|
|
5
5
|
import { detail } from './detail';
|
|
6
|
+
export var InteractiveAllowed;
|
|
7
|
+
(function (InteractiveAllowed) {
|
|
8
|
+
InteractiveAllowed["ALLOWED"] = "allowed";
|
|
9
|
+
InteractiveAllowed["DISALLOWED"] = "disallowed";
|
|
10
|
+
})(InteractiveAllowed || (InteractiveAllowed = {}));
|
|
6
11
|
/**
|
|
7
12
|
* Background class for Gamification feature
|
|
8
13
|
*/
|
|
@@ -13,6 +18,11 @@ export class GamificationBackground {
|
|
|
13
18
|
organizationId;
|
|
14
19
|
/** current user id */
|
|
15
20
|
userId;
|
|
21
|
+
/**
|
|
22
|
+
* flag to allow interactive questions (polls, trivia, predictions)
|
|
23
|
+
* controlled by opt-in and onboarding flags
|
|
24
|
+
*/
|
|
25
|
+
interactiveAllowed;
|
|
16
26
|
/** opened question, using to download statistics */
|
|
17
27
|
openedQuestionId;
|
|
18
28
|
/** opened question statistics */
|
|
@@ -36,10 +46,11 @@ export class GamificationBackground {
|
|
|
36
46
|
this.organizationId = instance.stores.organizationSettings.getAtomStore();
|
|
37
47
|
this.userId = instance.stores.user.getAtomStore();
|
|
38
48
|
this.moderationId = new SingleStore(createSingleStore(undefined), 'moderationId').getStore();
|
|
49
|
+
this.interactiveAllowed = new SingleStore(createSingleStore(InteractiveAllowed.DISALLOWED), 'interactiveAllowed').getStore();
|
|
39
50
|
this.openedQuestionId = new SingleStore(createSingleStore(undefined), 'openedQuestionId').getStore();
|
|
40
51
|
this.notifications = instance.notifications;
|
|
41
52
|
this.moderation = new ApiStore(queries.$moderation(this.slStreamId, instance.transport), 'gamification:moderation');
|
|
42
|
-
this.feedList = new ApiStore(queries.$feedList(this.slStreamId, instance.transport), 'gamification:feedList');
|
|
53
|
+
this.feedList = new ApiStore(queries.$feedList(this.slStreamId, this.interactiveAllowed, instance.transport), 'gamification:feedList');
|
|
43
54
|
this.activeQuestionId = new ApiStore(queries.$activeQuestion(this.slStreamId, instance.transport), 'gamification:activeQuestionId');
|
|
44
55
|
this.openedQuestion = detail(instance.transport, this.openedQuestionId, this.feedList.getStore());
|
|
45
56
|
this.openedQuestionId.listen((questionId) => {
|
|
@@ -82,6 +93,16 @@ export class GamificationBackground {
|
|
|
82
93
|
});
|
|
83
94
|
});
|
|
84
95
|
this.feedSubscription.connect();
|
|
96
|
+
/**
|
|
97
|
+
* invalidate active question on interactiveAllowed change
|
|
98
|
+
* close question if interactiveAllowed changed to disallowed
|
|
99
|
+
* open question if interactiveAllowed changed to allowed
|
|
100
|
+
*/
|
|
101
|
+
this.interactiveAllowed.listen(() => {
|
|
102
|
+
window.requestAnimationFrame(() => {
|
|
103
|
+
this.activeQuestionId.invalidate();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
85
106
|
}
|
|
86
107
|
/**
|
|
87
108
|
* Get id for notifications and link with current session
|
package/lib/gamification.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { leaderboard } from './leaderboard';
|
|
|
9
9
|
import { deepLink } from './deepLink';
|
|
10
10
|
import { OnboardingStatus } from './onboarding';
|
|
11
11
|
import { LeaderboardItem } from './queries/leaderboard';
|
|
12
|
-
import { GamificationBackground } from './';
|
|
12
|
+
import { GamificationBackground } from './background';
|
|
13
13
|
/**
|
|
14
14
|
* Gamification (Games) Overlay
|
|
15
15
|
* Includes:
|
|
@@ -24,6 +24,8 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
|
|
|
24
24
|
userSummary: ApiStore<GetApiResponseType<typeof queries.$userSummary>>;
|
|
25
25
|
/** feed list (pick history) */
|
|
26
26
|
feedList: ApiStore<GetApiResponseType<typeof queries.$feedList>>;
|
|
27
|
+
/** friends list */
|
|
28
|
+
friends: ApiStore<GetApiResponseType<typeof queries.$friends>>;
|
|
27
29
|
/** pinned leaderboard id */
|
|
28
30
|
leaderboardId: WritableAtom<string | undefined>;
|
|
29
31
|
/** leaderboard list */
|
|
@@ -49,6 +51,8 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
|
|
|
49
51
|
/** Browser cache */
|
|
50
52
|
private storage;
|
|
51
53
|
constructor(config: FeatureProps, source: FeatureSource, instance: StreamLayerContext);
|
|
54
|
+
get isInteractiveAllowed(): boolean;
|
|
55
|
+
checkInteractiveFlag: () => void;
|
|
52
56
|
connect: (transport: StreamLayerContext['transport']) => void;
|
|
53
57
|
disconnect: () => void;
|
|
54
58
|
submitAnswer: (questionId: string, answerId: string) => Promise<void>;
|
|
@@ -57,4 +61,11 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
|
|
|
57
61
|
closeQuestion: (questionId?: string) => void;
|
|
58
62
|
openUser: (userId: string) => void;
|
|
59
63
|
closeUser: () => void;
|
|
64
|
+
/**
|
|
65
|
+
* Show in-app notification for active question
|
|
66
|
+
* for interactive questions we show notification only if interactiveAllowed
|
|
67
|
+
* for factoid and tweet questions we show notification always
|
|
68
|
+
* skipping questions with inAppSilence === ON
|
|
69
|
+
*/
|
|
70
|
+
private showInApp;
|
|
60
71
|
}
|
package/lib/gamification.js
CHANGED
|
@@ -8,8 +8,8 @@ import { GamificationStorage } from './storage';
|
|
|
8
8
|
import { leaderboard } from './leaderboard';
|
|
9
9
|
import { deepLink } from './deepLink';
|
|
10
10
|
import { OnboardingStatus, onboarding } from './onboarding';
|
|
11
|
-
import { GamificationBackground } from './';
|
|
12
|
-
const
|
|
11
|
+
import { GamificationBackground, InteractiveAllowed } from './background';
|
|
12
|
+
const InteractiveQuestionTypes = new Set([QuestionType.POLL, QuestionType.PREDICTION, QuestionType.TRIVIA]);
|
|
13
13
|
/**
|
|
14
14
|
* Gamification (Games) Overlay
|
|
15
15
|
* Includes:
|
|
@@ -24,6 +24,8 @@ export class Gamification extends AbstractFeature {
|
|
|
24
24
|
userSummary;
|
|
25
25
|
/** feed list (pick history) */
|
|
26
26
|
feedList;
|
|
27
|
+
/** friends list */
|
|
28
|
+
friends;
|
|
27
29
|
/** pinned leaderboard id */
|
|
28
30
|
leaderboardId;
|
|
29
31
|
/** leaderboard list */
|
|
@@ -51,6 +53,7 @@ export class Gamification extends AbstractFeature {
|
|
|
51
53
|
this.storage = new GamificationStorage();
|
|
52
54
|
this.userSummary = new ApiStore(queries.$userSummary(this.background.slStreamId, this.background.userId, instance.transport), 'gamification:userSummary');
|
|
53
55
|
this.feedList = this.background.feedList;
|
|
56
|
+
this.friends = new ApiStore(queries.$friends(this.background.userId, instance.transport), 'gamification:friends');
|
|
54
57
|
this.currentUserId = this.background.userId;
|
|
55
58
|
this.openedUser = createSingleStore(undefined);
|
|
56
59
|
this.leaderboardId = new SingleStore(createSingleStore(this.settings.getValue('pinnedLeaderboardId')), 'pinnedLeaderboardId').getStore();
|
|
@@ -61,7 +64,7 @@ export class Gamification extends AbstractFeature {
|
|
|
61
64
|
this.openFeature = () => instance.sdk.openFeature(FeatureType.GAMES);
|
|
62
65
|
this.openedQuestion = this.background.openedQuestion;
|
|
63
66
|
this.deepLink = deepLink(this.transport, this.background.slStreamId, this.background.userId);
|
|
64
|
-
this.leaderboardList = leaderboard(this.transport, this.background.slStreamId);
|
|
67
|
+
this.leaderboardList = leaderboard(this.transport, this.background.slStreamId, this.background.userId, this.friends);
|
|
65
68
|
this.status.subscribe((status) => {
|
|
66
69
|
if (status === FeatureStatus.Ready) {
|
|
67
70
|
this.connect(instance.transport);
|
|
@@ -70,97 +73,45 @@ export class Gamification extends AbstractFeature {
|
|
|
70
73
|
this.disconnect();
|
|
71
74
|
}
|
|
72
75
|
});
|
|
76
|
+
// 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
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
73
84
|
/**
|
|
74
85
|
* listen for active question and show in-app notification
|
|
75
86
|
*/
|
|
76
|
-
this.background.activeQuestionId.listen(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (GamificationQuestionTypes.has(question.data.question.type)) {
|
|
84
|
-
const onboardingEnabled = this.background.moderation.getStore().value?.data?.options?.onboardingEnabled;
|
|
85
|
-
const optInEnabled = this.settings.getValues().inplayGame?.titleCard?.optIn;
|
|
86
|
-
const onboardingCompleted = onboardingStatus === OnboardingStatus.Completed;
|
|
87
|
-
if (!onboardingEnabled || onboardingCompleted || optInEnabled !== true) {
|
|
88
|
-
this.notifications.add({
|
|
89
|
-
type: NotificationType.QUESTION,
|
|
90
|
-
action: () => question.data?.question && this.openQuestion(question.data.question.id),
|
|
91
|
-
close: () => question.data?.question && this.closeQuestion(question.data.question.id),
|
|
92
|
-
autoHideDuration: 1000 * 60,
|
|
93
|
-
id: this.background.getCurrentSessionId({
|
|
94
|
-
prefix: 'notification',
|
|
95
|
-
entity: question.data.question.id,
|
|
96
|
-
}),
|
|
97
|
-
data: {
|
|
98
|
-
questionType: question.data.question.type,
|
|
99
|
-
question: {
|
|
100
|
-
title: question.data.question.notification.title,
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
else if (question.data.question.type === QuestionType.FACTOID) {
|
|
107
|
-
const optionsValue = question.data.question.options?.options.value;
|
|
108
|
-
const instantView = {
|
|
109
|
-
heading: question.data.question.notification.title,
|
|
110
|
-
body: question.data.question.notification.body,
|
|
111
|
-
image: question.data.question.notification.image,
|
|
112
|
-
video: {
|
|
113
|
-
id: optionsValue?.video?.id || '',
|
|
114
|
-
url: optionsValue?.video?.url || '',
|
|
115
|
-
thumbnailUrl: optionsValue?.video?.thumbnailUrl || '',
|
|
116
|
-
},
|
|
117
|
-
webLink: {
|
|
118
|
-
label: '',
|
|
119
|
-
url: '',
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
this.notifications.add({
|
|
123
|
-
type: NotificationType.QUESTION,
|
|
124
|
-
action: () => question.data?.question && this.openQuestion(question.data.question.id),
|
|
125
|
-
close: () => question.data?.question && this.closeQuestion(question.data.question.id),
|
|
126
|
-
autoHideDuration: 1000 * 120,
|
|
127
|
-
id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
|
|
128
|
-
data: {
|
|
129
|
-
questionType: question.data.question.type,
|
|
130
|
-
insight: instantView,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
else if (question.data.question.type === QuestionType.TWEET) {
|
|
135
|
-
const optionsValue = question.data.question.options?.options.value;
|
|
136
|
-
const tweetView = {
|
|
137
|
-
title: question.data.question.notification.title,
|
|
138
|
-
body: question.data.question.notification.body,
|
|
139
|
-
image: question.data.question.notification.image,
|
|
140
|
-
account: optionsValue?.tweetMeta?.account || '',
|
|
141
|
-
accountVerified: !!optionsValue?.tweetMeta?.accountVerified,
|
|
142
|
-
};
|
|
143
|
-
this.notifications.add({
|
|
144
|
-
type: NotificationType.QUESTION,
|
|
145
|
-
action: () => question.data?.question && this.openQuestion(question.data.question.id),
|
|
146
|
-
close: () => question.data?.question && this.closeQuestion(question.data.question.id),
|
|
147
|
-
autoHideDuration: 1000 * 120,
|
|
148
|
-
id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
|
|
149
|
-
data: {
|
|
150
|
-
questionType: question.data.question.type,
|
|
151
|
-
tweet: tweetView,
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
});
|
|
87
|
+
this.background.activeQuestionId.listen(this.showInApp);
|
|
88
|
+
/**
|
|
89
|
+
* listen for onboarding status, moderation onboarding changes and opt-in settings
|
|
90
|
+
*/
|
|
91
|
+
this.onboardingStatus.$store.listen(this.checkInteractiveFlag);
|
|
92
|
+
this.background.moderation.getStore().listen(this.checkInteractiveFlag);
|
|
93
|
+
this.settings.subscribe(this.checkInteractiveFlag);
|
|
158
94
|
}
|
|
95
|
+
get isInteractiveAllowed() {
|
|
96
|
+
return this.background.interactiveAllowed.get() === InteractiveAllowed.ALLOWED;
|
|
97
|
+
}
|
|
98
|
+
checkInteractiveFlag = () => {
|
|
99
|
+
const onboardingStatus = this.onboardingStatus.$store.get();
|
|
100
|
+
const onboardingEnabled = this.background.moderation.getStore().value?.data?.options?.onboardingEnabled;
|
|
101
|
+
const optInEnabled = this.settings.getValues().inplayGame?.titleCard?.optIn;
|
|
102
|
+
const onboardingCompleted = onboardingStatus === OnboardingStatus.Completed;
|
|
103
|
+
const allowed = !onboardingEnabled || onboardingCompleted || optInEnabled !== true;
|
|
104
|
+
this.background.interactiveAllowed.set(allowed ? InteractiveAllowed.ALLOWED : InteractiveAllowed.DISALLOWED);
|
|
105
|
+
};
|
|
159
106
|
connect = (transport) => {
|
|
160
107
|
this.userSummary.invalidate();
|
|
161
108
|
this.leaderboardList.invalidate();
|
|
162
109
|
this.feedList.invalidate();
|
|
110
|
+
this.friends.invalidate();
|
|
163
111
|
this.background.feedSubscription.addListener('feed-subscription-prediction-close', (response) => {
|
|
112
|
+
if (!this.isInteractiveAllowed) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
164
115
|
window.requestAnimationFrame(async () => {
|
|
165
116
|
const question = response.data?.attributes?.question;
|
|
166
117
|
if (!question) {
|
|
@@ -241,4 +192,89 @@ export class Gamification extends AbstractFeature {
|
|
|
241
192
|
closeUser = () => {
|
|
242
193
|
this.openedUser.set(undefined);
|
|
243
194
|
};
|
|
195
|
+
/**
|
|
196
|
+
* Show in-app notification for active question
|
|
197
|
+
* for interactive questions we show notification only if interactiveAllowed
|
|
198
|
+
* for factoid and tweet questions we show notification always
|
|
199
|
+
* skipping questions with inAppSilence === ON
|
|
200
|
+
*/
|
|
201
|
+
showInApp = (question) => {
|
|
202
|
+
const onboardingStatus = this.onboardingStatus.$store.get();
|
|
203
|
+
if (question && question.data && onboardingStatus && onboardingStatus !== OnboardingStatus.Unset) {
|
|
204
|
+
if (question.data.question?.id !== undefined &&
|
|
205
|
+
question.data.question.notification !== undefined &&
|
|
206
|
+
question.data.question?.bypassNotifications?.inAppSilence !== SilenceSetting.ON &&
|
|
207
|
+
question.data.question.status === QuestionStatus.ACTIVE) {
|
|
208
|
+
if (InteractiveQuestionTypes.has(question.data.question.type)) {
|
|
209
|
+
if (this.isInteractiveAllowed) {
|
|
210
|
+
this.notifications.add({
|
|
211
|
+
type: NotificationType.QUESTION,
|
|
212
|
+
action: () => question.data?.question && this.openQuestion(question.data.question.id),
|
|
213
|
+
close: () => question.data?.question && this.closeQuestion(question.data.question.id),
|
|
214
|
+
autoHideDuration: 1000 * 60,
|
|
215
|
+
id: this.background.getCurrentSessionId({
|
|
216
|
+
prefix: 'notification',
|
|
217
|
+
entity: question.data.question.id,
|
|
218
|
+
}),
|
|
219
|
+
data: {
|
|
220
|
+
questionType: question.data.question.type,
|
|
221
|
+
question: {
|
|
222
|
+
title: question.data.question.notification.title,
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else if (question.data.question.type === QuestionType.FACTOID) {
|
|
229
|
+
const optionsValue = question.data.question.options?.options.value;
|
|
230
|
+
const instantView = {
|
|
231
|
+
heading: question.data.question.notification.title,
|
|
232
|
+
body: question.data.question.notification.body,
|
|
233
|
+
image: optionsValue.image,
|
|
234
|
+
video: {
|
|
235
|
+
id: optionsValue?.video?.id || '',
|
|
236
|
+
url: optionsValue?.video?.url || '',
|
|
237
|
+
thumbnailUrl: optionsValue?.video?.thumbnailUrl || '',
|
|
238
|
+
},
|
|
239
|
+
webLink: {
|
|
240
|
+
label: '',
|
|
241
|
+
url: '',
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
this.notifications.add({
|
|
245
|
+
type: NotificationType.QUESTION,
|
|
246
|
+
action: () => question.data?.question && this.openQuestion(question.data.question.id),
|
|
247
|
+
close: () => question.data?.question && this.closeQuestion(question.data.question.id),
|
|
248
|
+
autoHideDuration: 1000 * 120,
|
|
249
|
+
id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
|
|
250
|
+
data: {
|
|
251
|
+
questionType: question.data.question.type,
|
|
252
|
+
insight: instantView,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
else if (question.data.question.type === QuestionType.TWEET) {
|
|
257
|
+
const optionsValue = question.data.question.options?.options.value;
|
|
258
|
+
const tweetView = {
|
|
259
|
+
title: question.data.question.notification.title,
|
|
260
|
+
body: question.data.question.notification.body,
|
|
261
|
+
image: question.data.question.notification.image,
|
|
262
|
+
account: optionsValue?.tweetMeta?.account || '',
|
|
263
|
+
accountVerified: !!optionsValue?.tweetMeta?.accountVerified,
|
|
264
|
+
};
|
|
265
|
+
this.notifications.add({
|
|
266
|
+
type: NotificationType.QUESTION,
|
|
267
|
+
action: () => question.data?.question && this.openQuestion(question.data.question.id),
|
|
268
|
+
close: () => question.data?.question && this.closeQuestion(question.data.question.id),
|
|
269
|
+
autoHideDuration: 1000 * 120,
|
|
270
|
+
id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
|
|
271
|
+
data: {
|
|
272
|
+
questionType: question.data.question.type,
|
|
273
|
+
tweet: tweetView,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
244
280
|
}
|
package/lib/leaderboard.d.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import type { Transport } from '@streamlayer/sdk-web-api';
|
|
2
2
|
import { ReadableAtom } from 'nanostores';
|
|
3
3
|
import { type LeaderboardItem } from './queries/leaderboard';
|
|
4
|
+
import { Gamification } from './';
|
|
4
5
|
type LeaderboardOptions = {
|
|
5
6
|
pageSize?: number;
|
|
6
7
|
};
|
|
7
8
|
type LeaderboardStore = {
|
|
8
|
-
data
|
|
9
|
+
data: LeaderboardItem[];
|
|
9
10
|
loading?: boolean;
|
|
11
|
+
key: number;
|
|
10
12
|
hasMore: boolean;
|
|
11
13
|
error?: string;
|
|
12
14
|
};
|
|
13
|
-
export declare const leaderboard: (transport: Transport, $eventId: ReadableAtom<string | undefined>, options?: LeaderboardOptions) => {
|
|
15
|
+
export declare const leaderboard: (transport: Transport, $eventId: ReadableAtom<string | undefined>, $userId: ReadableAtom<string | undefined>, $friends: Gamification['friends'], options?: LeaderboardOptions) => {
|
|
14
16
|
$store: import("nanostores").MapStore<LeaderboardStore>;
|
|
15
17
|
fetchMore: (page?: number) => void;
|
|
16
18
|
invalidate: () => void;
|
package/lib/leaderboard.js
CHANGED
|
@@ -4,25 +4,43 @@ import { createLeaderboardListFetch } from './queries/leaderboard';
|
|
|
4
4
|
const defaultOptions = {
|
|
5
5
|
pageSize: 10,
|
|
6
6
|
};
|
|
7
|
-
export const leaderboard = (transport, $eventId, options) => {
|
|
7
|
+
export const leaderboard = (transport, $eventId, $userId, $friends, options) => {
|
|
8
8
|
let maxPage = 0;
|
|
9
9
|
const $pagination = createSingleStore({ pageSize: options?.pageSize || defaultOptions.pageSize, page: 0 });
|
|
10
|
-
const $store = createMapStore({
|
|
10
|
+
const $store = createMapStore({
|
|
11
|
+
data: [],
|
|
12
|
+
key: Date.now(),
|
|
13
|
+
hasMore: true,
|
|
14
|
+
loading: undefined,
|
|
15
|
+
error: undefined,
|
|
16
|
+
});
|
|
11
17
|
const fetch = createLeaderboardListFetch(transport);
|
|
12
18
|
/**
|
|
13
19
|
* Refetch leaderboard data, reset pagination
|
|
14
20
|
*/
|
|
15
|
-
const refetch = async (
|
|
16
|
-
|
|
21
|
+
const refetch = async () => {
|
|
22
|
+
const eventId = $eventId.get();
|
|
23
|
+
const friendsIds = $friends
|
|
24
|
+
.getStore()
|
|
25
|
+
.get()
|
|
26
|
+
.data?.map((friend) => friend.slId) || [];
|
|
27
|
+
if (eventId && friendsIds?.length) {
|
|
28
|
+
const userId = $userId.get();
|
|
29
|
+
if (userId) {
|
|
30
|
+
friendsIds.push(userId);
|
|
31
|
+
}
|
|
17
32
|
$store.setKey('loading', true);
|
|
33
|
+
$store.setKey('data', []);
|
|
18
34
|
const request = {
|
|
19
35
|
eventId: eventId,
|
|
36
|
+
usersIds: friendsIds,
|
|
20
37
|
pagination: { page: 0, pageSize: options?.pageSize || defaultOptions.pageSize },
|
|
21
38
|
};
|
|
22
39
|
const newData = await fetch(request);
|
|
23
40
|
$store.set({
|
|
24
41
|
data: newData.data.map((item) => item.attributes),
|
|
25
42
|
hasMore: true,
|
|
43
|
+
key: Date.now(),
|
|
26
44
|
loading: false,
|
|
27
45
|
});
|
|
28
46
|
if (newData.meta) {
|
|
@@ -32,7 +50,7 @@ export const leaderboard = (transport, $eventId, options) => {
|
|
|
32
50
|
}
|
|
33
51
|
};
|
|
34
52
|
const invalidate = () => {
|
|
35
|
-
void refetch(
|
|
53
|
+
void refetch();
|
|
36
54
|
};
|
|
37
55
|
/**
|
|
38
56
|
* Whether there is more data to fetch
|
|
@@ -46,6 +64,7 @@ export const leaderboard = (transport, $eventId, options) => {
|
|
|
46
64
|
};
|
|
47
65
|
onMount($store, () => {
|
|
48
66
|
const cancelRefetchListener = $eventId.listen(refetch);
|
|
67
|
+
const cancelRefetchByFriendsListener = $friends.listen(refetch);
|
|
49
68
|
const cancelPaginationListener = $pagination.listen(async (pagination) => {
|
|
50
69
|
const eventId = $eventId.get();
|
|
51
70
|
if (pagination.page > 0 && eventId) {
|
|
@@ -59,6 +78,7 @@ export const leaderboard = (transport, $eventId, options) => {
|
|
|
59
78
|
const prevData = $store.get().data || [];
|
|
60
79
|
$store.set({
|
|
61
80
|
data: [...prevData, ...newData.data.map((item) => item.attributes)],
|
|
81
|
+
key: $store.get().key,
|
|
62
82
|
loading: false,
|
|
63
83
|
hasMore: true,
|
|
64
84
|
});
|
|
@@ -70,6 +90,7 @@ export const leaderboard = (transport, $eventId, options) => {
|
|
|
70
90
|
});
|
|
71
91
|
return () => {
|
|
72
92
|
cancelRefetchListener();
|
|
93
|
+
cancelRefetchByFriendsListener();
|
|
73
94
|
cancelPaginationListener();
|
|
74
95
|
};
|
|
75
96
|
});
|
package/lib/onboarding.js
CHANGED
|
@@ -28,9 +28,6 @@ const showOnboardingInApp = (service, background, notifications, storage) => {
|
|
|
28
28
|
type: NotificationType.ONBOARDING,
|
|
29
29
|
id: notificationId,
|
|
30
30
|
action: service.openFeature,
|
|
31
|
-
close: () => {
|
|
32
|
-
notifications.markAsViewed(notificationId);
|
|
33
|
-
},
|
|
34
31
|
persistent: true,
|
|
35
32
|
autoHideDuration: 1000000,
|
|
36
33
|
data: {
|
|
@@ -100,6 +97,8 @@ const onboardingProcess = ($store, background, service, notifications, storage,
|
|
|
100
97
|
}
|
|
101
98
|
};
|
|
102
99
|
export const onboarding = (service, background, transport, notifications) => {
|
|
100
|
+
// show onboarding each time when user open new sdk session
|
|
101
|
+
let onboardingShowed = false;
|
|
103
102
|
const storage = new GamificationStorage();
|
|
104
103
|
const $store = createSingleStore(OnboardingStatus.Unset);
|
|
105
104
|
$store.subscribe((onboardingStatus) => {
|
|
@@ -107,7 +106,10 @@ export const onboarding = (service, background, transport, notifications) => {
|
|
|
107
106
|
return;
|
|
108
107
|
}
|
|
109
108
|
if (onboardingStatus === OnboardingStatus.Optional || onboardingStatus === OnboardingStatus.Required) {
|
|
110
|
-
|
|
109
|
+
if (onboardingShowed === false) {
|
|
110
|
+
showOnboardingInApp(service, background, notifications, storage);
|
|
111
|
+
onboardingShowed = true;
|
|
112
|
+
}
|
|
111
113
|
}
|
|
112
114
|
if (onboardingStatus === OnboardingStatus.Completed) {
|
|
113
115
|
background.activeQuestionId.invalidate();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Transport } from '@streamlayer/sdk-web-api';
|
|
2
|
+
import { ReadableAtom } from 'nanostores';
|
|
3
|
+
export declare const $friends: ($userId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sl-eslib/users/users_pb").GetFriendsResponse_FriendData[], any>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Users } from '@streamlayer/sl-eslib/users/users_connect';
|
|
2
|
+
export const $friends = ($userId, transport) => {
|
|
3
|
+
const { client, queryKey } = transport.createPromiseClient(Users, {
|
|
4
|
+
method: 'getFriends',
|
|
5
|
+
params: [$userId],
|
|
6
|
+
});
|
|
7
|
+
return transport.nanoquery.createFetcherStore(queryKey, {
|
|
8
|
+
fetcher: async (_, __, userId) => {
|
|
9
|
+
if (!userId) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
const res = await client.getFriends({});
|
|
13
|
+
return res.data;
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
};
|
package/lib/queries/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Transport } from '@streamlayer/sdk-web-api';
|
|
2
2
|
import { ReadableAtom } from 'nanostores';
|
|
3
3
|
import type { SubscriptionRequest, SubscriptionResponse, VotingSubscriptionRequest, VotingSubscriptionResponse, QuestionSubscriptionRequest, QuestionSubscriptionResponse } from '@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb';
|
|
4
|
+
import { InteractiveAllowed } from '../background';
|
|
4
5
|
export declare const $activeQuestion: (slStreamId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedQuestion | undefined, any>;
|
|
5
6
|
export declare const feedSubscription: ($slStreamId: ReadableAtom<string | undefined>, 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<{
|
|
6
7
|
readonly typeName: "streamlayer.interactive.feed.Feed";
|
|
@@ -330,7 +331,7 @@ export declare const getQuestionByUser: (questionId: string, transport: Transpor
|
|
|
330
331
|
export declare const getQuestionDetail: (questionId: string, transport: Transport) => Promise<import("@streamlayer/sl-eslib/interactive/interactive.common_pb").Question | undefined>;
|
|
331
332
|
export declare const $questionByUser: ($questionId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sdk-web-types").ExtendedQuestion | undefined, any>;
|
|
332
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>;
|
|
333
|
-
export declare const $feedList: (slStreamId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").FeedItem[], any>;
|
|
334
|
-
export declare const $insightHistory: (slStreamId: ReadableAtom<string | undefined>, transport: Transport) => import("@nanostores/query").FetcherStore<(import("@streamlayer/sl-eslib/interactive/feed/interactive.feed_pb").InsightHistory | undefined)[] | undefined, any>;
|
|
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';
|
|
336
|
+
export { $friends } from './friends';
|
|
336
337
|
export { $moderation } from './moderation';
|
package/lib/queries/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { QuestionStatus } from '@streamlayer/sdk-web-types';
|
|
1
|
+
import { QuestionStatus, QuestionType } from '@streamlayer/sdk-web-types';
|
|
2
2
|
import { atom } from 'nanostores';
|
|
3
3
|
import { Feed } from '@streamlayer/sl-eslib/interactive/feed/interactive.feed_connect';
|
|
4
|
+
import { InteractiveAllowed } from '../background';
|
|
4
5
|
export const $activeQuestion = (slStreamId, transport) => {
|
|
5
6
|
const { client, queryKey } = transport.createPromiseClient(Feed, { method: 'syncQuestion', params: [slStreamId] });
|
|
6
7
|
return transport.nanoquery.createFetcherStore(queryKey, {
|
|
@@ -75,16 +76,22 @@ export const $pickHistory = (slStreamId, transport) => {
|
|
|
75
76
|
},
|
|
76
77
|
});
|
|
77
78
|
};
|
|
78
|
-
export const $feedList = (slStreamId, transport) => {
|
|
79
|
-
const { client, queryKey } = transport.createPromiseClient(Feed, {
|
|
79
|
+
export const $feedList = ($slStreamId, $interactiveAllowed, transport) => {
|
|
80
|
+
const { client, queryKey } = transport.createPromiseClient(Feed, {
|
|
81
|
+
method: 'list',
|
|
82
|
+
params: [$slStreamId, $interactiveAllowed],
|
|
83
|
+
});
|
|
80
84
|
return transport.nanoquery.createFetcherStore(queryKey, {
|
|
81
|
-
fetcher: async (_, __,
|
|
82
|
-
if (!
|
|
85
|
+
fetcher: async (_, __, slStreamId, interactiveAllowed) => {
|
|
86
|
+
if (!slStreamId) {
|
|
83
87
|
return [];
|
|
84
88
|
}
|
|
85
89
|
const res = await client.list({
|
|
86
|
-
eventId:
|
|
90
|
+
eventId: slStreamId,
|
|
87
91
|
filter: {
|
|
92
|
+
types: interactiveAllowed === InteractiveAllowed.ALLOWED
|
|
93
|
+
? [] // allow all types
|
|
94
|
+
: [QuestionType.FACTOID, QuestionType.PROMOTION, QuestionType.TWEET],
|
|
88
95
|
statuses: [QuestionStatus.ACTIVE, QuestionStatus.RESOLVED],
|
|
89
96
|
},
|
|
90
97
|
});
|
|
@@ -92,19 +99,6 @@ export const $feedList = (slStreamId, transport) => {
|
|
|
92
99
|
},
|
|
93
100
|
});
|
|
94
101
|
};
|
|
95
|
-
export const $insightHistory = (slStreamId, transport) => {
|
|
96
|
-
const { client, queryKey } = transport.createPromiseClient(Feed, { method: 'insightHistory', params: [slStreamId] });
|
|
97
|
-
return transport.nanoquery.createFetcherStore(queryKey, {
|
|
98
|
-
fetcher: async (_, __, eventId) => {
|
|
99
|
-
if (!eventId) {
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
const res = await client.insightHistory({
|
|
103
|
-
eventId: eventId,
|
|
104
|
-
});
|
|
105
|
-
return res.data?.map(({ attributes }) => attributes);
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
};
|
|
109
102
|
export { $userSummary, $leaderboardList } from './leaderboard';
|
|
103
|
+
export { $friends } from './friends';
|
|
110
104
|
export { $moderation } from './moderation';
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamlayer/feature-gamification",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"@bufbuild/protobuf": "^1.6.0",
|
|
6
6
|
"@fastify/deepmerge": "*",
|
|
7
7
|
"@streamlayer/sl-eslib": "^5.63.3",
|
|
8
8
|
"nanostores": "^0.9.5",
|
|
9
|
-
"@streamlayer/sdk-web-api": "^0.
|
|
10
|
-
"@streamlayer/sdk-web-core": "^0.
|
|
11
|
-
"@streamlayer/sdk-web-interfaces": "^0.
|
|
12
|
-
"@streamlayer/sdk-web-logger": "^0.5.
|
|
13
|
-
"@streamlayer/sdk-web-notifications": "^0.13.
|
|
14
|
-
"@streamlayer/sdk-web-storage": "^0.3.
|
|
15
|
-
"@streamlayer/sdk-web-types": "^0.
|
|
9
|
+
"@streamlayer/sdk-web-api": "^0.20.0",
|
|
10
|
+
"@streamlayer/sdk-web-core": "^0.18.1",
|
|
11
|
+
"@streamlayer/sdk-web-interfaces": "^0.20.0",
|
|
12
|
+
"@streamlayer/sdk-web-logger": "^0.5.11",
|
|
13
|
+
"@streamlayer/sdk-web-notifications": "^0.13.6",
|
|
14
|
+
"@streamlayer/sdk-web-storage": "^0.3.11",
|
|
15
|
+
"@streamlayer/sdk-web-types": "^0.21.0"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"tslib": "^2.6.2"
|