@streamlayer/feature-gamification 0.38.0 → 0.39.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 +7 -2
- package/lib/background.js +8 -4
- package/lib/constants.d.ts +4 -0
- package/lib/constants.js +5 -0
- package/lib/detail.d.ts +7 -9
- package/lib/detail.js +36 -87
- package/lib/gamification.d.ts +8 -3
- package/lib/gamification.js +219 -52
- package/lib/onboarding.js +7 -2
- package/lib/queries/actions.js +13 -2
- package/lib/queries/deepLink.js +2 -0
- package/lib/queries/index.d.ts +1 -1
- package/lib/queries/index.js +5 -0
- package/lib/queries/moderation.js +2 -0
- package/package.json +11 -11
package/lib/background.d.ts
CHANGED
|
@@ -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
|
|
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
|
@@ -40,7 +40,9 @@ export class GamificationBackground {
|
|
|
40
40
|
questionSubscription;
|
|
41
41
|
notifications;
|
|
42
42
|
log;
|
|
43
|
+
transport;
|
|
43
44
|
constructor(instance) {
|
|
45
|
+
this.transport = instance.transport;
|
|
44
46
|
this.log = createLogger('gamification-background');
|
|
45
47
|
this.slStreamId = instance.stores.slStreamId.getAtomStore();
|
|
46
48
|
this.organizationId = instance.stores.organizationSettings.getAtomStore();
|
|
@@ -58,9 +60,7 @@ export class GamificationBackground {
|
|
|
58
60
|
if (item?.questionId) {
|
|
59
61
|
this.questionSubscription = queries.questionSubscription(item.questionId, instance.transport);
|
|
60
62
|
this.questionSubscription.addListener('feed-subscription-opened-question', (response) => {
|
|
61
|
-
|
|
62
|
-
this.openedQuestion.updateExtendedQuestion(response.data?.attributes?.question);
|
|
63
|
-
});
|
|
63
|
+
this.openedQuestion.updateExtendedQuestion(response.data?.attributes?.question);
|
|
64
64
|
});
|
|
65
65
|
this.questionSubscription.connect();
|
|
66
66
|
}
|
|
@@ -77,7 +77,6 @@ export class GamificationBackground {
|
|
|
77
77
|
this.feedSubscription.addListener('feed-subscription-active-question', (response) => {
|
|
78
78
|
const $activeQuestionId = this.activeQuestionId.getStore();
|
|
79
79
|
if ($activeQuestionId) {
|
|
80
|
-
this.activeQuestionId;
|
|
81
80
|
$activeQuestionId.mutate(response.data?.attributes);
|
|
82
81
|
}
|
|
83
82
|
});
|
|
@@ -121,6 +120,11 @@ export class GamificationBackground {
|
|
|
121
120
|
};
|
|
122
121
|
disconnect = () => {
|
|
123
122
|
this.feedSubscription?.disconnect();
|
|
123
|
+
if (this.questionSubscription !== undefined) {
|
|
124
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
125
|
+
this.transport.removeSubscription(this.questionSubscription);
|
|
126
|
+
this.questionSubscription = undefined;
|
|
127
|
+
}
|
|
124
128
|
};
|
|
125
129
|
/**
|
|
126
130
|
* Open question and mark notification for this question as viewed
|
package/lib/constants.js
ADDED
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("
|
|
15
|
-
|
|
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,45 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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 =
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
+
answers: mergeQuestionAnswers(currentQuestion?.answers, question?.answers),
|
|
42
|
+
});
|
|
94
43
|
};
|
|
95
44
|
return { $store, $extendedStore, updateExtendedQuestion };
|
|
96
45
|
};
|
package/lib/gamification.d.ts
CHANGED
|
@@ -44,20 +44,25 @@ export declare class Gamification extends AbstractFeature<'games', PlainMessage<
|
|
|
44
44
|
openedUser: WritableAtom<LeaderboardItem | undefined>;
|
|
45
45
|
closeFeature: () => void;
|
|
46
46
|
openFeature: () => void;
|
|
47
|
+
feedSubscription: GamificationBackground['feedSubscription'];
|
|
48
|
+
activeQuestionId: GamificationBackground['activeQuestionId'];
|
|
49
|
+
openedQuestionId: GamificationBackground['openedQuestionId'];
|
|
47
50
|
private notifications;
|
|
48
51
|
private transport;
|
|
49
52
|
/** gamification background class, handle subscriptions and notifications for closed overlay */
|
|
50
53
|
private background;
|
|
51
54
|
/** Browser cache */
|
|
52
55
|
private storage;
|
|
56
|
+
private submitAnswerTimeout;
|
|
53
57
|
constructor(config: FeatureProps, source: FeatureSource, instance: StreamLayerContext);
|
|
54
58
|
get isInteractiveAllowed(): boolean;
|
|
55
59
|
checkInteractiveFlag: () => void;
|
|
56
|
-
connect: (
|
|
60
|
+
connect: () => void;
|
|
57
61
|
disconnect: () => void;
|
|
58
62
|
submitAnswer: (questionId: string, answerId: string) => Promise<void>;
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
openQuestion: (questionId: string, question?: FeedItem & {
|
|
64
|
+
openedFrom?: 'list' | 'notification';
|
|
65
|
+
}) => void;
|
|
61
66
|
closeQuestion: (questionId?: string) => void;
|
|
62
67
|
openUser: (userId: string) => void;
|
|
63
68
|
closeUser: () => void;
|
package/lib/gamification.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
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,8 @@ 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';
|
|
12
15
|
const InteractiveQuestionTypes = new Set([QuestionType.POLL, QuestionType.PREDICTION, QuestionType.TRIVIA]);
|
|
13
16
|
/**
|
|
14
17
|
* Gamification (Games) Overlay
|
|
@@ -41,15 +44,22 @@ export class Gamification extends AbstractFeature {
|
|
|
41
44
|
openedUser;
|
|
42
45
|
closeFeature;
|
|
43
46
|
openFeature;
|
|
47
|
+
feedSubscription;
|
|
48
|
+
activeQuestionId;
|
|
49
|
+
openedQuestionId;
|
|
44
50
|
notifications;
|
|
45
51
|
transport;
|
|
46
52
|
/** gamification background class, handle subscriptions and notifications for closed overlay */
|
|
47
53
|
background;
|
|
48
54
|
/** Browser cache */
|
|
49
55
|
storage;
|
|
56
|
+
submitAnswerTimeout;
|
|
50
57
|
constructor(config, source, instance) {
|
|
51
58
|
super(config, source);
|
|
52
59
|
this.background = new GamificationBackground(instance);
|
|
60
|
+
this.feedSubscription = this.background.feedSubscription;
|
|
61
|
+
this.activeQuestionId = this.background.activeQuestionId;
|
|
62
|
+
this.openedQuestionId = this.background.openedQuestionId;
|
|
53
63
|
this.storage = new GamificationStorage();
|
|
54
64
|
this.userSummary = new ApiStore(queries.$userSummary(this.background.slStreamId, this.background.userId, instance.transport), 'gamification:userSummary');
|
|
55
65
|
this.feedList = this.background.feedList;
|
|
@@ -65,20 +75,11 @@ export class Gamification extends AbstractFeature {
|
|
|
65
75
|
this.openedQuestion = this.background.openedQuestion;
|
|
66
76
|
this.deepLink = deepLink(this.transport, this.background.slStreamId, instance.stores.providerStreamId.getStore(), this.background.userId);
|
|
67
77
|
this.leaderboardList = leaderboard(this.transport, this.background.slStreamId, this.background.userId, this.friends);
|
|
68
|
-
this.
|
|
69
|
-
if (status === FeatureStatus.Ready) {
|
|
70
|
-
this.connect(instance.transport);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
this.disconnect();
|
|
74
|
-
}
|
|
75
|
-
});
|
|
78
|
+
this.connect();
|
|
76
79
|
// refresh leaderboard on user summary update after earning points
|
|
77
80
|
this.userSummary.listen((userSummary) => {
|
|
78
81
|
if (this.leaderboardList.$store.lc !== 0 && userSummary?.data?.summary) {
|
|
79
|
-
|
|
80
|
-
this.leaderboardList.invalidate();
|
|
81
|
-
});
|
|
82
|
+
this.leaderboardList.invalidate(); // verified, it's necessary
|
|
82
83
|
}
|
|
83
84
|
});
|
|
84
85
|
/**
|
|
@@ -103,41 +104,79 @@ export class Gamification extends AbstractFeature {
|
|
|
103
104
|
const allowed = !onboardingEnabled || onboardingCompleted || optInEnabled !== true;
|
|
104
105
|
this.background.interactiveAllowed.set(allowed ? InteractiveAllowed.ALLOWED : InteractiveAllowed.DISALLOWED);
|
|
105
106
|
};
|
|
106
|
-
connect = (
|
|
107
|
-
this.
|
|
108
|
-
this.leaderboardList.invalidate();
|
|
109
|
-
this.feedList.invalidate();
|
|
110
|
-
this.friends.invalidate();
|
|
111
|
-
this.background.feedSubscription.addListener('feed-subscription-prediction-close', (response) => {
|
|
107
|
+
connect = () => {
|
|
108
|
+
this.background.feedSubscription.addListener('feed-subscription-prediction-close', async (response) => {
|
|
112
109
|
if (!this.isInteractiveAllowed) {
|
|
113
110
|
return;
|
|
114
111
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
112
|
+
const question = response.data?.attributes?.question;
|
|
113
|
+
const feedItem = response.data?.attributes?.feedItem;
|
|
114
|
+
if (!question || !feedItem?.attributes) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const { status, type, id, answers } = question;
|
|
118
|
+
if (status === QuestionStatus.RESOLVED && type === QuestionType.PREDICTION) {
|
|
119
|
+
const notificationId = this.background.getCurrentSessionId({
|
|
120
|
+
prefix: `notification-id:${id}`,
|
|
121
|
+
});
|
|
122
|
+
const feedList = [...(this.feedList.getValues().data || [])];
|
|
123
|
+
const questionFromFeedListIndex = feedList.findIndex((item) => item.id === id);
|
|
124
|
+
const questionFromFeedList = feedList[questionFromFeedListIndex];
|
|
125
|
+
// @ts-ignore
|
|
126
|
+
let votedAnswerId = questionFromFeedList?.attributes?.attributes?.value?.answerId;
|
|
127
|
+
// get voted answer id from extended question or feed list
|
|
128
|
+
const data = $questionByUser(id, this.transport);
|
|
129
|
+
// order of operations is important here
|
|
130
|
+
const cancel = data.subscribe(() => { });
|
|
131
|
+
await data.get().promise;
|
|
132
|
+
const extendedQuestion = data.get().data;
|
|
133
|
+
cancel();
|
|
134
|
+
window.requestAnimationFrame(() => {
|
|
135
|
+
data.invalidate();
|
|
136
|
+
});
|
|
137
|
+
// get extended question data and mark as dirty
|
|
138
|
+
if (!votedAnswerId) {
|
|
139
|
+
votedAnswerId = extendedQuestion?.answers.find(({ youVoted }) => youVoted)?.id;
|
|
120
140
|
}
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
const correctAnswer = answers.find(({ correct }) => correct);
|
|
142
|
+
const votedAnswer = votedAnswerId ? answers.find(({ id }) => id === votedAnswerId) : undefined;
|
|
143
|
+
const votedCorrect = !!votedAnswer?.correct;
|
|
144
|
+
// update question in feed list if it's there
|
|
145
|
+
if (questionFromFeedList) {
|
|
146
|
+
if (feedList[questionFromFeedListIndex]?.attributes?.attributes.case === 'question') {
|
|
147
|
+
try {
|
|
148
|
+
// @ts-ignore
|
|
149
|
+
feedList[questionFromFeedListIndex].attributes.attributes.value.answerId = votedAnswerId;
|
|
150
|
+
// @ts-ignore
|
|
151
|
+
feedList[questionFromFeedListIndex].attributes.attributes.value.openForVoting = false;
|
|
152
|
+
if (votedAnswerId) {
|
|
153
|
+
// @ts-ignore
|
|
154
|
+
feedList[questionFromFeedListIndex].attributes.attributes.value.status = votedCorrect
|
|
155
|
+
? PickHistoryStatus.WON
|
|
156
|
+
: PickHistoryStatus.LOST;
|
|
157
|
+
}
|
|
158
|
+
// eslint-disable-next-line no-empty
|
|
159
|
+
}
|
|
160
|
+
catch (e) { }
|
|
161
|
+
this.feedList.getStore().mutate(feedList);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (!votedAnswer || !correctAnswer)
|
|
165
|
+
return;
|
|
166
|
+
// avoid showing notification if question already opened
|
|
167
|
+
if (this.openedQuestionId.get()?.questionId !== question.id) {
|
|
131
168
|
this.notifications.add({
|
|
132
169
|
type: NotificationType.QUESTION_RESOLVED,
|
|
133
170
|
action: () => this.openQuestion(question.id, feedItem),
|
|
134
171
|
close: () => this.closeQuestion(id),
|
|
135
|
-
autoHideDuration:
|
|
172
|
+
autoHideDuration: votedCorrect ? 15000 : 12000,
|
|
136
173
|
id: notificationId,
|
|
174
|
+
emitEvent: false,
|
|
137
175
|
data: {
|
|
176
|
+
questionId: id,
|
|
138
177
|
questionType: QuestionType.PREDICTION,
|
|
139
178
|
question: {
|
|
140
|
-
title:
|
|
179
|
+
title: votedCorrect
|
|
141
180
|
? `Congratulations! You answered correctly! You won ${correctAnswer.points} pts!`
|
|
142
181
|
: `Better luck next time! Correct: ${correctAnswer?.text}!`,
|
|
143
182
|
votedAnswer: {
|
|
@@ -145,41 +184,163 @@ export class Gamification extends AbstractFeature {
|
|
|
145
184
|
points: votedAnswer?.points,
|
|
146
185
|
},
|
|
147
186
|
correctAnswerTitle: correctAnswer?.text,
|
|
148
|
-
correct:
|
|
187
|
+
correct: !!votedCorrect,
|
|
149
188
|
predictionResult: status === QuestionStatus.RESOLVED,
|
|
150
189
|
questionTitle: question?.subject,
|
|
151
190
|
},
|
|
152
191
|
},
|
|
153
192
|
});
|
|
154
|
-
this.userSummary.invalidate();
|
|
155
193
|
}
|
|
156
|
-
|
|
194
|
+
this.userSummary.invalidate(); // verified, it's necessary
|
|
195
|
+
}
|
|
157
196
|
});
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
197
|
+
// update feed list on question update received from subscription
|
|
198
|
+
// add new question to the top of the list
|
|
199
|
+
this.background.feedSubscription.addListener('feed-subscription-questions-list', (response) => {
|
|
200
|
+
const feedList = [...(this.feedList.getStore().value?.data || [])];
|
|
201
|
+
const feedItem = response.data?.attributes?.feedItem;
|
|
202
|
+
const questionIndex = feedList.findIndex((item) => item.id === feedItem?.id);
|
|
203
|
+
if (!feedItem) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (questionIndex !== -1) {
|
|
207
|
+
if (feedItem.attributes?.attributes.case === 'question' &&
|
|
208
|
+
feedList[questionIndex].attributes?.attributes.case === 'question') {
|
|
209
|
+
const prev = feedList[questionIndex];
|
|
210
|
+
if (prev.attributes) {
|
|
211
|
+
feedList[questionIndex] = {
|
|
212
|
+
...feedList[questionIndex],
|
|
213
|
+
attributes: {
|
|
214
|
+
...prev.attributes,
|
|
215
|
+
attributes: {
|
|
216
|
+
...prev.attributes.attributes,
|
|
217
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
218
|
+
// @ts-ignore
|
|
219
|
+
value: {
|
|
220
|
+
...prev.attributes.attributes.value,
|
|
221
|
+
...feedItem.attributes.attributes.value,
|
|
222
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
223
|
+
// @ts-ignore
|
|
224
|
+
answerId: prev.attributes.attributes.value.answerId,
|
|
225
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
226
|
+
// @ts-ignore
|
|
227
|
+
status: prev.attributes.attributes.value.status,
|
|
228
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
229
|
+
// @ts-ignore
|
|
230
|
+
openForVoting: prev.attributes.attributes.value.openForVoting,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
feedList[questionIndex] = feedItem;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (questionIndex === -1) {
|
|
242
|
+
feedList.unshift(feedItem);
|
|
243
|
+
eventBus.emit('poll', {
|
|
244
|
+
action: 'received',
|
|
245
|
+
payload: {
|
|
246
|
+
questionId: feedItem.id,
|
|
247
|
+
questionType: feedItem.attributes?.type,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
this.feedList.getStore().mutate(feedList);
|
|
162
252
|
});
|
|
163
253
|
};
|
|
254
|
+
// not used
|
|
164
255
|
disconnect = () => {
|
|
256
|
+
this.background.feedSubscription.removeListener('feed-subscription-prediction-close');
|
|
165
257
|
this.background.feedSubscription.removeListener('feed-subscription-questions-list');
|
|
166
258
|
};
|
|
167
259
|
submitAnswer = async (questionId, answerId) => {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
260
|
+
const data = $questionByUser(questionId, this.transport);
|
|
261
|
+
const updateQuestionAndFieldList = () => {
|
|
262
|
+
const feedList = this.feedList.getValues().data;
|
|
263
|
+
if (!feedList) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const questionIndex = feedList.findIndex((item) => item.id === questionId);
|
|
267
|
+
const poll = feedList[questionIndex];
|
|
268
|
+
const question = poll?.attributes?.attributes.case === 'question' && poll.attributes.attributes.value;
|
|
269
|
+
if (question) {
|
|
270
|
+
eventBus.emit('poll', {
|
|
271
|
+
action: 'voted',
|
|
272
|
+
payload: {
|
|
273
|
+
questionId,
|
|
274
|
+
questionType: question.questionType,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
const cancel = data.subscribe(() => { });
|
|
278
|
+
const extendedQuestion = data.get().data;
|
|
279
|
+
cancel();
|
|
280
|
+
if (extendedQuestion) {
|
|
281
|
+
const correctAnswer = extendedQuestion.answers.find((answer) => answer.correct === true);
|
|
282
|
+
const votedAnswerIdx = extendedQuestion.answers.findIndex((answer) => answer.id === answerId);
|
|
283
|
+
const votedAnswer = extendedQuestion.answers[votedAnswerIdx];
|
|
284
|
+
// @ts-ignore
|
|
285
|
+
feedList[questionIndex].attributes.attributes.value.answerId = answerId;
|
|
286
|
+
// @ts-ignore
|
|
287
|
+
feedList[questionIndex].attributes.attributes.value.openForVoting = false;
|
|
288
|
+
// @ts-ignore
|
|
289
|
+
feedList[questionIndex].attributes.attributes.value.text = votedAnswer?.text || '';
|
|
290
|
+
if (correctAnswer) {
|
|
291
|
+
// @ts-ignore
|
|
292
|
+
feedList[questionIndex].attributes.attributes.value.status =
|
|
293
|
+
correctAnswer.id === answerId ? PickHistoryStatus.WON : PickHistoryStatus.LOST;
|
|
294
|
+
}
|
|
295
|
+
this.feedList.getStore().mutate([...feedList]);
|
|
296
|
+
extendedQuestion.answers[votedAnswerIdx].correct = correctAnswer?.id === answerId;
|
|
297
|
+
extendedQuestion.answers[votedAnswerIdx].youVoted = true;
|
|
298
|
+
if (correctAnswer?.id === answerId) {
|
|
299
|
+
extendedQuestion.answers[votedAnswerIdx].pointsEarned =
|
|
300
|
+
extendedQuestion.status === QuestionStatus.RESOLVED ? 0 : correctAnswer.points;
|
|
301
|
+
}
|
|
302
|
+
data.mutate({ ...extendedQuestion });
|
|
303
|
+
}
|
|
304
|
+
if (this.submitAnswerTimeout) {
|
|
305
|
+
clearTimeout(this.submitAnswerTimeout);
|
|
306
|
+
}
|
|
307
|
+
this.submitAnswerTimeout = setTimeout(() => {
|
|
308
|
+
this.userSummary.invalidate(); // verified, it's necessary
|
|
309
|
+
}, 1000);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
try {
|
|
313
|
+
await actions.submitAnswer(this.transport, { questionId, answerId });
|
|
314
|
+
updateQuestionAndFieldList();
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
if (error.message === ERROR.ALREADY_VOTED) {
|
|
318
|
+
this.userSummary.invalidate();
|
|
319
|
+
const cancel = data.subscribe(() => { });
|
|
320
|
+
data.invalidate();
|
|
321
|
+
cancel();
|
|
322
|
+
}
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
177
325
|
};
|
|
178
326
|
openQuestion = (questionId, question) => {
|
|
179
327
|
this.notifications.close(this.background.getCurrentSessionId({
|
|
180
328
|
prefix: 'notification',
|
|
181
329
|
entity: questionId,
|
|
182
330
|
}));
|
|
331
|
+
let questionType = question?.attributes?.type;
|
|
332
|
+
if (!questionType) {
|
|
333
|
+
const feedList = this.feedList.getStore().value?.data || [];
|
|
334
|
+
questionType = feedList.find((item) => item.id === questionId)?.attributes?.type;
|
|
335
|
+
}
|
|
336
|
+
eventBus.emit('poll', {
|
|
337
|
+
action: 'opened',
|
|
338
|
+
payload: {
|
|
339
|
+
questionId,
|
|
340
|
+
questionType,
|
|
341
|
+
questionOpenedFrom: question?.openedFrom,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
183
344
|
return this.background.openQuestion(questionId, question);
|
|
184
345
|
};
|
|
185
346
|
closeQuestion = (questionId) => {
|
|
@@ -217,7 +378,9 @@ export class Gamification extends AbstractFeature {
|
|
|
217
378
|
prefix: 'notification',
|
|
218
379
|
entity: question.data.question.id,
|
|
219
380
|
}),
|
|
381
|
+
emitEvent: true,
|
|
220
382
|
data: {
|
|
383
|
+
questionId: question.data.question.id,
|
|
221
384
|
questionType: question.data.question.type,
|
|
222
385
|
question: {
|
|
223
386
|
title: question.data.question.notification.title,
|
|
@@ -248,8 +411,10 @@ export class Gamification extends AbstractFeature {
|
|
|
248
411
|
action: () => question.data?.question && this.openQuestion(question.data.question.id, question.data.feedItem),
|
|
249
412
|
close: () => question.data?.question && this.closeQuestion(question.data.question.id),
|
|
250
413
|
autoHideDuration: 1000 * 120,
|
|
414
|
+
emitEvent: true,
|
|
251
415
|
id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
|
|
252
416
|
data: {
|
|
417
|
+
questionId: question.data.question.id,
|
|
253
418
|
questionType: question.data.question.type,
|
|
254
419
|
insight: instantView,
|
|
255
420
|
},
|
|
@@ -271,8 +436,10 @@ export class Gamification extends AbstractFeature {
|
|
|
271
436
|
action: () => question.data?.question && this.openQuestion(question.data.question.id, question.data.feedItem),
|
|
272
437
|
close: () => question.data?.question && this.closeQuestion(question.data.question.id),
|
|
273
438
|
autoHideDuration: 1000 * 120,
|
|
439
|
+
emitEvent: true,
|
|
274
440
|
id: this.background.getCurrentSessionId({ prefix: 'notification', entity: question.data.question.id }),
|
|
275
441
|
data: {
|
|
442
|
+
questionId: question.data.question.id,
|
|
276
443
|
questionType: question.data.question.type,
|
|
277
444
|
tweet: tweetView,
|
|
278
445
|
},
|
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
|
}
|
package/lib/queries/actions.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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' });
|
package/lib/queries/deepLink.js
CHANGED
package/lib/queries/index.d.ts
CHANGED
|
@@ -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
|
|
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';
|
package/lib/queries/index.js
CHANGED
|
@@ -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';
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamlayer/feature-gamification",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"peerDependencies": {
|
|
5
5
|
"@bufbuild/protobuf": "^1.7.2",
|
|
6
|
-
"@fastify/deepmerge": "
|
|
7
|
-
"@streamlayer/sl-eslib": "^5.
|
|
8
|
-
"nanostores": "^0.
|
|
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.
|
|
14
|
-
"@streamlayer/sdk-web-storage": "^0.4.
|
|
15
|
-
"@streamlayer/sdk-web-types": "^0.
|
|
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.0",
|
|
10
|
+
"@streamlayer/sdk-web-core": "^0.22.0",
|
|
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"
|