@speakableio/core 0.1.2 → 0.1.3
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/dist/index.d.mts +816 -55
- package/dist/index.d.ts +816 -55
- package/dist/index.js +701 -25
- package/dist/index.mjs +699 -23
- package/package.json +15 -9
package/dist/index.mjs
CHANGED
|
@@ -47,13 +47,21 @@ var FirebaseAPI = class _FirebaseAPI {
|
|
|
47
47
|
this.config = config;
|
|
48
48
|
}
|
|
49
49
|
get db() {
|
|
50
|
-
if (!this.config) throw new Error("
|
|
50
|
+
if (!this.config) throw new Error("Firebase API not initialized");
|
|
51
51
|
return this.config.db;
|
|
52
52
|
}
|
|
53
53
|
get helpers() {
|
|
54
|
-
if (!this.config) throw new Error("
|
|
54
|
+
if (!this.config) throw new Error("Firebase API not initialized");
|
|
55
55
|
return this.config.helpers;
|
|
56
56
|
}
|
|
57
|
+
get functions() {
|
|
58
|
+
if (!this.config) throw new Error("Firebase API not initialized");
|
|
59
|
+
return this.config.functions;
|
|
60
|
+
}
|
|
61
|
+
httpsCallable(functionName) {
|
|
62
|
+
if (!this.config) throw new Error("Firebase API httpsCallable not initialized");
|
|
63
|
+
return this.config.httpsCallable(this.functions, functionName);
|
|
64
|
+
}
|
|
57
65
|
accessQueryConstraints() {
|
|
58
66
|
const { query, orderBy, limit, startAt, startAfter, endAt, endBefore } = this.helpers;
|
|
59
67
|
return {
|
|
@@ -66,13 +74,27 @@ var FirebaseAPI = class _FirebaseAPI {
|
|
|
66
74
|
endBefore
|
|
67
75
|
};
|
|
68
76
|
}
|
|
77
|
+
accessHelpers() {
|
|
78
|
+
const { doc, collection, writeBatch, serverTimestamp, setDoc } = this.helpers;
|
|
79
|
+
return {
|
|
80
|
+
doc: (path) => doc(this.db, path),
|
|
81
|
+
collection: (path) => collection(this.db, path),
|
|
82
|
+
writeBatch: () => writeBatch(this.db),
|
|
83
|
+
serverTimestamp,
|
|
84
|
+
setDoc
|
|
85
|
+
};
|
|
86
|
+
}
|
|
69
87
|
async getDoc(path) {
|
|
70
88
|
const { getDoc, doc } = this.helpers;
|
|
71
89
|
const docRef = doc(this.db, path);
|
|
72
90
|
const docSnap = await getDoc(docRef);
|
|
91
|
+
const data = docSnap.exists() ? {
|
|
92
|
+
id: docSnap.id,
|
|
93
|
+
...docSnap.data()
|
|
94
|
+
} : null;
|
|
73
95
|
return {
|
|
74
96
|
id: docSnap.id,
|
|
75
|
-
data
|
|
97
|
+
data
|
|
76
98
|
};
|
|
77
99
|
}
|
|
78
100
|
async getDocs(path, ...queryConstraints) {
|
|
@@ -93,12 +115,15 @@ var FirebaseAPI = class _FirebaseAPI {
|
|
|
93
115
|
const { addDoc, collection } = this.helpers;
|
|
94
116
|
const collectionRef = collection(this.db, path);
|
|
95
117
|
const docRef = await addDoc(collectionRef, data);
|
|
96
|
-
return
|
|
118
|
+
return {
|
|
119
|
+
id: docRef.id,
|
|
120
|
+
...data
|
|
121
|
+
};
|
|
97
122
|
}
|
|
98
|
-
async setDoc(path, data) {
|
|
123
|
+
async setDoc(path, data, options = {}) {
|
|
99
124
|
const { setDoc, doc } = this.helpers;
|
|
100
125
|
const docRef = doc(this.db, path);
|
|
101
|
-
await setDoc(docRef, data);
|
|
126
|
+
await setDoc(docRef, data, options);
|
|
102
127
|
}
|
|
103
128
|
async updateDoc(path, data) {
|
|
104
129
|
const { updateDoc, doc } = this.helpers;
|
|
@@ -120,6 +145,11 @@ var FirebaseAPI = class _FirebaseAPI {
|
|
|
120
145
|
await Promise.all(operations.map((op) => op()));
|
|
121
146
|
await batch.commit();
|
|
122
147
|
}
|
|
148
|
+
writeBatch() {
|
|
149
|
+
const { writeBatch } = this.helpers;
|
|
150
|
+
const batch = writeBatch(this.db);
|
|
151
|
+
return batch;
|
|
152
|
+
}
|
|
123
153
|
};
|
|
124
154
|
var api = FirebaseAPI.getInstance();
|
|
125
155
|
|
|
@@ -136,10 +166,10 @@ var ANALYTICS_SUBCOLLECTION = "analytics";
|
|
|
136
166
|
var SCORES_SUBCOLLECTION = "scores";
|
|
137
167
|
var refsAssignmentFiresotre = {
|
|
138
168
|
allAssignments: () => ASSIGNMENTS_COLLECTION,
|
|
139
|
-
assignment: (
|
|
140
|
-
assignmentAllAnalytics: (
|
|
141
|
-
assignmentAnalytics: (
|
|
142
|
-
assignmentScores: (
|
|
169
|
+
assignment: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}`,
|
|
170
|
+
assignmentAllAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}`,
|
|
171
|
+
assignmentAnalytics: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${ANALYTICS_SUBCOLLECTION}/${params.type}`,
|
|
172
|
+
assignmentScores: (params) => `${ASSIGNMENTS_COLLECTION}/${params.id}/${SCORES_SUBCOLLECTION}/${params.userId}`
|
|
143
173
|
};
|
|
144
174
|
|
|
145
175
|
// src/domains/assignment/services/get-assignments-score.service.ts
|
|
@@ -150,21 +180,30 @@ var _getAssignmentScores = async ({
|
|
|
150
180
|
currentUserId
|
|
151
181
|
}) => {
|
|
152
182
|
if (analyticType === "student" /* Student */) {
|
|
153
|
-
const path = refsAssignmentFiresotre.assignmentScores(
|
|
183
|
+
const path = refsAssignmentFiresotre.assignmentScores({
|
|
184
|
+
id: assignmentId,
|
|
185
|
+
userId: currentUserId
|
|
186
|
+
});
|
|
154
187
|
const response = await api.getDoc(path);
|
|
155
188
|
return { scores: response.data, id: assignmentId };
|
|
156
189
|
}
|
|
157
190
|
if (analyticType === "student_summary" /* StudentSummary */ && studentId) {
|
|
158
|
-
const path = refsAssignmentFiresotre.assignmentScores(
|
|
191
|
+
const path = refsAssignmentFiresotre.assignmentScores({
|
|
192
|
+
id: assignmentId,
|
|
193
|
+
userId: studentId
|
|
194
|
+
});
|
|
159
195
|
const response = await api.getDoc(path);
|
|
160
196
|
return { scores: response.data, id: assignmentId };
|
|
161
197
|
}
|
|
162
198
|
if (analyticType !== "all" /* All */ && ASSIGNMENT_ANALYTICS_TYPES.includes(analyticType)) {
|
|
163
|
-
const ref = refsAssignmentFiresotre.assignmentAnalytics(
|
|
199
|
+
const ref = refsAssignmentFiresotre.assignmentAnalytics({
|
|
200
|
+
id: assignmentId,
|
|
201
|
+
type: analyticType
|
|
202
|
+
});
|
|
164
203
|
const docData = await api.getDoc(ref);
|
|
165
204
|
return { scores: docData.data, id: assignmentId };
|
|
166
205
|
} else if (analyticType === "all" /* All */) {
|
|
167
|
-
const ref = refsAssignmentFiresotre.assignmentAllAnalytics(assignmentId);
|
|
206
|
+
const ref = refsAssignmentFiresotre.assignmentAllAnalytics({ id: assignmentId });
|
|
168
207
|
const response = await api.getDocs(ref);
|
|
169
208
|
const data = response.data.reduce((acc, curr) => {
|
|
170
209
|
acc[curr.id] = curr;
|
|
@@ -230,7 +269,7 @@ var checkAssignmentAvailability = (scheduledTime) => {
|
|
|
230
269
|
|
|
231
270
|
// src/domains/assignment/services/get-assignment.service.ts
|
|
232
271
|
async function _getAssignment(params) {
|
|
233
|
-
const path = refsAssignmentFiresotre.assignment(params.assignmentId);
|
|
272
|
+
const path = refsAssignmentFiresotre.assignment({ id: params.assignmentId });
|
|
234
273
|
const response = await api.getDoc(path);
|
|
235
274
|
if (!response.data) return null;
|
|
236
275
|
const assignment = response.data;
|
|
@@ -263,7 +302,7 @@ var createAssignmentRepo = () => {
|
|
|
263
302
|
};
|
|
264
303
|
};
|
|
265
304
|
|
|
266
|
-
// src/domains/assignment/assignment.hooks.ts
|
|
305
|
+
// src/domains/assignment/hooks/assignment.hooks.ts
|
|
267
306
|
import { useQuery } from "@tanstack/react-query";
|
|
268
307
|
var assignmentQueryKeys = {
|
|
269
308
|
all: ["assignments"],
|
|
@@ -288,16 +327,609 @@ function useAssignment({
|
|
|
288
327
|
});
|
|
289
328
|
}
|
|
290
329
|
|
|
330
|
+
// src/domains/cards/card.hooks.ts
|
|
331
|
+
import { useMutation, useQueries } from "@tanstack/react-query";
|
|
332
|
+
import { useMemo } from "react";
|
|
333
|
+
|
|
334
|
+
// src/domains/cards/card.constants.ts
|
|
335
|
+
var FeedbackTypesCard = /* @__PURE__ */ ((FeedbackTypesCard2) => {
|
|
336
|
+
FeedbackTypesCard2["SuggestedResponse"] = "suggested_response";
|
|
337
|
+
FeedbackTypesCard2["Wida"] = "wida";
|
|
338
|
+
FeedbackTypesCard2["GrammarInsights"] = "grammar_insights";
|
|
339
|
+
FeedbackTypesCard2["Actfl"] = "actfl";
|
|
340
|
+
FeedbackTypesCard2["ProficiencyLevel"] = "proficiency_level";
|
|
341
|
+
return FeedbackTypesCard2;
|
|
342
|
+
})(FeedbackTypesCard || {});
|
|
343
|
+
var LeniencyCard = /* @__PURE__ */ ((LeniencyCard2) => {
|
|
344
|
+
LeniencyCard2["CONFIDENCE"] = "confidence";
|
|
345
|
+
LeniencyCard2["EASY"] = "easy";
|
|
346
|
+
LeniencyCard2["NORMAL"] = "normal";
|
|
347
|
+
LeniencyCard2["HARD"] = "hard";
|
|
348
|
+
return LeniencyCard2;
|
|
349
|
+
})(LeniencyCard || {});
|
|
350
|
+
var LENIENCY_OPTIONS = [
|
|
351
|
+
{
|
|
352
|
+
label: "Build Confidence - most lenient",
|
|
353
|
+
value: "confidence" /* CONFIDENCE */
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
label: "Very Lenient",
|
|
357
|
+
value: "easy" /* EASY */
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
label: "Normal",
|
|
361
|
+
value: "normal" /* NORMAL */
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
label: "No leniency - most strict",
|
|
365
|
+
value: "hard" /* HARD */
|
|
366
|
+
}
|
|
367
|
+
];
|
|
368
|
+
var STUDENT_LEVELS_OPTIONS = [
|
|
369
|
+
{
|
|
370
|
+
label: "Beginner",
|
|
371
|
+
description: "Beginner Level: Just starting out. Can say a few basic words and phrases.",
|
|
372
|
+
value: "beginner"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
label: "Elementary",
|
|
376
|
+
description: "Elementary Level: Can understand simple sentences and have very basic conversations.",
|
|
377
|
+
value: "elementary"
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
label: "Intermediate",
|
|
381
|
+
description: "Intermediate Level: Can talk about everyday topics and handle common situations.",
|
|
382
|
+
value: "intermediate"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
label: "Advanced",
|
|
386
|
+
description: "Advanced Level: Can speak and understand with ease, and explain ideas clearly.",
|
|
387
|
+
value: "advanced"
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
label: "Fluent",
|
|
391
|
+
description: "Fluent Level: Speaks naturally and easily. Can use the language in work or school settings.",
|
|
392
|
+
value: "fluent"
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
label: "Native-like",
|
|
396
|
+
description: "Native-like Level: Understands and speaks like a native. Can discuss complex ideas accurately.",
|
|
397
|
+
value: "nativeLike"
|
|
398
|
+
}
|
|
399
|
+
];
|
|
400
|
+
var BASE_RESPOND_FIELD_VALUES = {
|
|
401
|
+
title: "",
|
|
402
|
+
allowRetries: true,
|
|
403
|
+
respondTime: 180,
|
|
404
|
+
maxCharacters: 1e3
|
|
405
|
+
};
|
|
406
|
+
var BASE_REPEAT_FIELD_VALUES = {
|
|
407
|
+
repeat: 1
|
|
408
|
+
};
|
|
409
|
+
var BASE_MULTIPLE_CHOICE_FIELD_VALUES = {
|
|
410
|
+
MCQType: "single",
|
|
411
|
+
answer: ["A"],
|
|
412
|
+
choices: [
|
|
413
|
+
{ option: "A", value: "Option A" },
|
|
414
|
+
{ option: "B", value: "Option B" },
|
|
415
|
+
{ option: "C", value: "Option C" }
|
|
416
|
+
]
|
|
417
|
+
};
|
|
418
|
+
var VerificationCardStatus = /* @__PURE__ */ ((VerificationCardStatus2) => {
|
|
419
|
+
VerificationCardStatus2["VERIFIED"] = "VERIFIED";
|
|
420
|
+
VerificationCardStatus2["WARNING"] = "WARNING";
|
|
421
|
+
VerificationCardStatus2["NOT_RECOMMENDED"] = "NOT_RECOMMENDED";
|
|
422
|
+
VerificationCardStatus2["NOT_WORKING"] = "NOT_WORKING";
|
|
423
|
+
VerificationCardStatus2["NOT_CHECKED"] = "NOT_CHECKED";
|
|
424
|
+
return VerificationCardStatus2;
|
|
425
|
+
})(VerificationCardStatus || {});
|
|
426
|
+
var CARDS_COLLECTION = "flashcards";
|
|
427
|
+
var refsCardsFiresotre = {
|
|
428
|
+
allCards: CARDS_COLLECTION,
|
|
429
|
+
card: (id) => `${CARDS_COLLECTION}/${id}`
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// src/domains/cards/services/get-card.service.ts
|
|
433
|
+
async function _getCard(params) {
|
|
434
|
+
const ref = refsCardsFiresotre.card(params.cardId);
|
|
435
|
+
const response = await api.getDoc(ref);
|
|
436
|
+
if (!response.data) return null;
|
|
437
|
+
return response.data;
|
|
438
|
+
}
|
|
439
|
+
var getCard = withErrorHandler(_getCard, "getCard");
|
|
440
|
+
|
|
441
|
+
// src/domains/cards/services/create-card.service.ts
|
|
442
|
+
import { v4 } from "uuid";
|
|
443
|
+
|
|
444
|
+
// src/domains/cards/card.model.ts
|
|
445
|
+
var CardActivityType = /* @__PURE__ */ ((CardActivityType2) => {
|
|
446
|
+
CardActivityType2["READ_REPEAT"] = "READ_REPEAT";
|
|
447
|
+
CardActivityType2["VIDEO"] = "VIDEO";
|
|
448
|
+
CardActivityType2["TEXT"] = "TEXT";
|
|
449
|
+
CardActivityType2["READ_RESPOND"] = "READ_RESPOND";
|
|
450
|
+
CardActivityType2["FREE_RESPONSE"] = "FREE_RESPONSE";
|
|
451
|
+
CardActivityType2["REPEAT"] = "REPEAT";
|
|
452
|
+
CardActivityType2["RESPOND"] = "RESPOND";
|
|
453
|
+
CardActivityType2["RESPOND_WRITE"] = "RESPOND_WRITE";
|
|
454
|
+
CardActivityType2["TEXT_TO_SPEECH"] = "TEXT_TO_SPEECH";
|
|
455
|
+
CardActivityType2["MULTIPLE_CHOICE"] = "MULTIPLE_CHOICE";
|
|
456
|
+
CardActivityType2["PODCAST"] = "PODCAST";
|
|
457
|
+
CardActivityType2["MEDIA_PAGE"] = "MEDIA_PAGE";
|
|
458
|
+
CardActivityType2["WRITE"] = "WRITE";
|
|
459
|
+
CardActivityType2["SHORT_ANSWER"] = "SHORT_ANSWER";
|
|
460
|
+
CardActivityType2["SHORT_STORY"] = "SHORT_STORY";
|
|
461
|
+
CardActivityType2["SPEAK"] = "SPEAK";
|
|
462
|
+
CardActivityType2["CONVERSATION"] = "CONVERSATION";
|
|
463
|
+
CardActivityType2["CONVERSATION_WRITE"] = "CONVERSATION_WRITE";
|
|
464
|
+
CardActivityType2["DIALOGUE"] = "DIALOGUE";
|
|
465
|
+
CardActivityType2["INSTRUCTION"] = "INSTRUCTION";
|
|
466
|
+
CardActivityType2["LISTEN"] = "LISTEN";
|
|
467
|
+
CardActivityType2["READ"] = "READ";
|
|
468
|
+
CardActivityType2["ANSWER"] = "ANSWER";
|
|
469
|
+
return CardActivityType2;
|
|
470
|
+
})(CardActivityType || {});
|
|
471
|
+
var RESPOND_CARD_ACTIVITY_TYPES = [
|
|
472
|
+
"READ_RESPOND" /* READ_RESPOND */,
|
|
473
|
+
"RESPOND" /* RESPOND */,
|
|
474
|
+
"RESPOND_WRITE" /* RESPOND_WRITE */,
|
|
475
|
+
"FREE_RESPONSE" /* FREE_RESPONSE */
|
|
476
|
+
];
|
|
477
|
+
var MULTIPLE_CHOICE_CARD_ACTIVITY_TYPES = ["MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */];
|
|
478
|
+
var REPEAT_CARD_ACTIVITY_TYPES = ["READ_REPEAT" /* READ_REPEAT */, "REPEAT" /* REPEAT */];
|
|
479
|
+
var RESPOND_WRITE_CARD_ACTIVITY_TYPES = [
|
|
480
|
+
"RESPOND_WRITE" /* RESPOND_WRITE */,
|
|
481
|
+
"FREE_RESPONSE" /* FREE_RESPONSE */
|
|
482
|
+
];
|
|
483
|
+
var RESPOND_AUDIO_CARD_ACTIVITY_TYPES = [
|
|
484
|
+
"RESPOND" /* RESPOND */,
|
|
485
|
+
"READ_RESPOND" /* READ_RESPOND */
|
|
486
|
+
];
|
|
487
|
+
var ALLOWED_CARD_ACTIVITY_TYPES_FOR_SUMMARY = [
|
|
488
|
+
"REPEAT" /* REPEAT */,
|
|
489
|
+
"RESPOND" /* RESPOND */,
|
|
490
|
+
"READ_REPEAT" /* READ_REPEAT */,
|
|
491
|
+
"READ_RESPOND" /* READ_RESPOND */,
|
|
492
|
+
"FREE_RESPONSE" /* FREE_RESPONSE */,
|
|
493
|
+
"RESPOND_WRITE" /* RESPOND_WRITE */,
|
|
494
|
+
"MULTIPLE_CHOICE" /* MULTIPLE_CHOICE */
|
|
495
|
+
];
|
|
496
|
+
|
|
497
|
+
// src/utils/text-utils.ts
|
|
498
|
+
import sha1 from "js-sha1";
|
|
499
|
+
var purify = (word) => {
|
|
500
|
+
return word.normalize("NFD").replace(/\/([^" "]*)/g, "").replace(/\([^()]*\)/g, "").replace(/([^()]*)/g, "").replace(/[\u0300-\u036f]/g, "").replace(/[-]/g, " ").replace(/[.,/#!¡¿?؟。,.?$%^&*;:{}=\-_`~()’'…\s]/g, "").replace(/\s\s+/g, " ").toLowerCase().trim();
|
|
501
|
+
};
|
|
502
|
+
var cleanString = (words) => {
|
|
503
|
+
const splitWords = words?.split("+");
|
|
504
|
+
if (splitWords && splitWords.length === 1) {
|
|
505
|
+
const newWord = purify(words);
|
|
506
|
+
return newWord;
|
|
507
|
+
} else if (splitWords && splitWords.length > 1) {
|
|
508
|
+
const split = splitWords.map((w) => purify(w));
|
|
509
|
+
return split;
|
|
510
|
+
} else {
|
|
511
|
+
return "";
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
var getWordHash = (word, language) => {
|
|
515
|
+
const cleanedWord = cleanString(word);
|
|
516
|
+
const wordHash = sha1(`${language}-${cleanedWord}`);
|
|
517
|
+
return wordHash;
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// src/domains/cards/services/get-card-verification-status.service.ts
|
|
521
|
+
var charactarLanguages = ["zh", "ja", "ko"];
|
|
522
|
+
var getVerificationStatus = async (target_text, language) => {
|
|
523
|
+
if (target_text?.length < 3 && !charactarLanguages.includes(language)) {
|
|
524
|
+
return "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
|
|
525
|
+
}
|
|
526
|
+
const hash = getWordHash(target_text, language);
|
|
527
|
+
const response = await api.getDoc(`checked-pronunciations/${hash}`);
|
|
528
|
+
try {
|
|
529
|
+
if (response.data) {
|
|
530
|
+
return processRecord(response.data);
|
|
531
|
+
} else {
|
|
532
|
+
return "NOT_CHECKED" /* NOT_CHECKED */;
|
|
533
|
+
}
|
|
534
|
+
} catch (e) {
|
|
535
|
+
return "NOT_CHECKED" /* NOT_CHECKED */;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
var processRecord = (data) => {
|
|
539
|
+
const { pronunciations = 0, fails = 0 } = data;
|
|
540
|
+
const attempts = pronunciations + fails;
|
|
541
|
+
const successRate = attempts > 0 ? pronunciations / attempts * 100 : 0;
|
|
542
|
+
let newStatus = null;
|
|
543
|
+
if (attempts < 6) {
|
|
544
|
+
return "NOT_CHECKED" /* NOT_CHECKED */;
|
|
545
|
+
}
|
|
546
|
+
if (successRate > 25) {
|
|
547
|
+
newStatus = "VERIFIED" /* VERIFIED */;
|
|
548
|
+
} else if (successRate > 10) {
|
|
549
|
+
newStatus = "WARNING" /* WARNING */;
|
|
550
|
+
} else if (fails > 20 && successRate < 10 && pronunciations > 1) {
|
|
551
|
+
newStatus = "NOT_RECOMMENDED" /* NOT_RECOMMENDED */;
|
|
552
|
+
} else if (pronunciations === 0 && fails > 20) {
|
|
553
|
+
newStatus = "NOT_WORKING" /* NOT_WORKING */;
|
|
554
|
+
} else {
|
|
555
|
+
newStatus = "NOT_CHECKED" /* NOT_CHECKED */;
|
|
556
|
+
}
|
|
557
|
+
return newStatus;
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
// src/domains/cards/services/create-card.service.ts
|
|
561
|
+
async function _createCard({ data }) {
|
|
562
|
+
const response = await api.addDoc(refsCardsFiresotre.allCards, data);
|
|
563
|
+
return response;
|
|
564
|
+
}
|
|
565
|
+
var createCard = withErrorHandler(_createCard, "createCard");
|
|
566
|
+
async function _createCards({ cards }) {
|
|
567
|
+
const { writeBatch, doc } = api.accessHelpers();
|
|
568
|
+
const batch = writeBatch();
|
|
569
|
+
const cardsWithId = [];
|
|
570
|
+
for (const card of cards) {
|
|
571
|
+
const cardId = v4();
|
|
572
|
+
const ref = doc(refsCardsFiresotre.card(cardId));
|
|
573
|
+
const newCardObject = {
|
|
574
|
+
...card,
|
|
575
|
+
id: cardId
|
|
576
|
+
};
|
|
577
|
+
if (card.type === "READ_REPEAT" /* READ_REPEAT */ && card.target_text && card.language) {
|
|
578
|
+
const verificationStatus = await getVerificationStatus(card.target_text, card.language);
|
|
579
|
+
newCardObject.verificationStatus = verificationStatus || null;
|
|
580
|
+
}
|
|
581
|
+
cardsWithId.push(newCardObject);
|
|
582
|
+
batch.set(ref, newCardObject);
|
|
583
|
+
}
|
|
584
|
+
await batch.commit();
|
|
585
|
+
return cardsWithId;
|
|
586
|
+
}
|
|
587
|
+
var createCards = withErrorHandler(_createCards, "createCards");
|
|
588
|
+
|
|
589
|
+
// src/domains/cards/card.hooks.ts
|
|
590
|
+
var cardsQueryKeys = {
|
|
591
|
+
all: ["cards"],
|
|
592
|
+
one: (params) => [...cardsQueryKeys.all, params.cardId]
|
|
593
|
+
};
|
|
594
|
+
function useCards({
|
|
595
|
+
cardIds,
|
|
596
|
+
enabled = true,
|
|
597
|
+
asObject
|
|
598
|
+
}) {
|
|
599
|
+
const queries = useQueries({
|
|
600
|
+
queries: cardIds.map((cardId) => ({
|
|
601
|
+
enabled: enabled && cardIds.length > 0,
|
|
602
|
+
queryKey: cardsQueryKeys.one({
|
|
603
|
+
cardId
|
|
604
|
+
}),
|
|
605
|
+
queryFn: () => getCard({ cardId })
|
|
606
|
+
}))
|
|
607
|
+
});
|
|
608
|
+
const cards = queries.map((query) => query.data).filter(Boolean);
|
|
609
|
+
const cardsObject = useMemo(() => {
|
|
610
|
+
if (!asObject) return null;
|
|
611
|
+
return cards.reduce((acc, card) => {
|
|
612
|
+
acc[card.id] = card;
|
|
613
|
+
return acc;
|
|
614
|
+
}, {});
|
|
615
|
+
}, [asObject, cards]);
|
|
616
|
+
return {
|
|
617
|
+
cards,
|
|
618
|
+
cardsObject,
|
|
619
|
+
cardsQueries: queries
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function useCreateCard() {
|
|
623
|
+
const { queryClient } = useSpeakableApi();
|
|
624
|
+
const mutationCreateCard = useMutation({
|
|
625
|
+
mutationFn: createCard,
|
|
626
|
+
onSuccess: (cardCreated) => {
|
|
627
|
+
queryClient.invalidateQueries({ queryKey: cardsQueryKeys.one({ cardId: cardCreated.id }) });
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
return {
|
|
631
|
+
mutationCreateCard
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
function useCreateCards() {
|
|
635
|
+
const mutationCreateCards = useMutation({
|
|
636
|
+
mutationFn: createCards
|
|
637
|
+
});
|
|
638
|
+
return {
|
|
639
|
+
mutationCreateCards
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
function getCardFromCache({
|
|
643
|
+
cardId,
|
|
644
|
+
queryClient
|
|
645
|
+
}) {
|
|
646
|
+
return queryClient.getQueryData(cardsQueryKeys.one({ cardId }));
|
|
647
|
+
}
|
|
648
|
+
function updateCardInCache({
|
|
649
|
+
cardId,
|
|
650
|
+
card,
|
|
651
|
+
queryClient
|
|
652
|
+
}) {
|
|
653
|
+
queryClient.setQueryData(cardsQueryKeys.one({ cardId }), card);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/domains/cards/card.repo.ts
|
|
657
|
+
var createCardRepo = () => {
|
|
658
|
+
return {
|
|
659
|
+
createCard,
|
|
660
|
+
createCards,
|
|
661
|
+
getCard
|
|
662
|
+
};
|
|
663
|
+
};
|
|
664
|
+
|
|
665
|
+
// src/domains/sets/set.hooks.ts
|
|
666
|
+
import { useQuery as useQuery2 } from "@tanstack/react-query";
|
|
667
|
+
|
|
668
|
+
// src/domains/sets/set.constants.ts
|
|
669
|
+
var SETS_COLLECTION = "sets";
|
|
670
|
+
var refsSetsFirestore = {
|
|
671
|
+
allSets: SETS_COLLECTION,
|
|
672
|
+
set: (id) => `${SETS_COLLECTION}/${id}`
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// src/domains/sets/services/get-set.service.ts
|
|
676
|
+
async function _getSet({ setId }) {
|
|
677
|
+
const response = await api.getDoc(refsSetsFirestore.set(setId));
|
|
678
|
+
return response.data;
|
|
679
|
+
}
|
|
680
|
+
var getSet = withErrorHandler(_getSet, "getSet");
|
|
681
|
+
|
|
682
|
+
// src/domains/sets/set.hooks.ts
|
|
683
|
+
var setsQueryKeys = {
|
|
684
|
+
all: ["sets"],
|
|
685
|
+
one: (params) => [...setsQueryKeys.all, params.setId]
|
|
686
|
+
};
|
|
687
|
+
var useSet = ({ setId, enabled }) => {
|
|
688
|
+
return useQuery2({
|
|
689
|
+
queryKey: setsQueryKeys.one({ setId }),
|
|
690
|
+
queryFn: () => getSet({ setId }),
|
|
691
|
+
enabled: setId !== void 0 && setId !== "" && enabled
|
|
692
|
+
});
|
|
693
|
+
};
|
|
694
|
+
function getSetFromCache({
|
|
695
|
+
setId,
|
|
696
|
+
queryClient
|
|
697
|
+
}) {
|
|
698
|
+
if (!setId) return null;
|
|
699
|
+
return queryClient.getQueryData(setsQueryKeys.one({ setId }));
|
|
700
|
+
}
|
|
701
|
+
function updateSetInCache({
|
|
702
|
+
set,
|
|
703
|
+
queryClient
|
|
704
|
+
}) {
|
|
705
|
+
const { id, ...setData } = set;
|
|
706
|
+
queryClient.setQueryData(setsQueryKeys.one({ setId: id }), setData);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/domains/sets/set.repo.ts
|
|
710
|
+
var createSetRepo = () => {
|
|
711
|
+
return {
|
|
712
|
+
getSet
|
|
713
|
+
};
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/domains/notification/notification.constants.ts
|
|
717
|
+
var SPEAKABLE_NOTIFICATIONS = {
|
|
718
|
+
NEW_ASSIGNMENT: "new_assignment",
|
|
719
|
+
ASSESSMENT_SUBMITTED: "assessment_submitted",
|
|
720
|
+
ASSESSMENT_SCORED: "assessment_scored",
|
|
721
|
+
NEW_COMMENT: "NEW_COMMENT"
|
|
722
|
+
};
|
|
723
|
+
var SpeakableNotificationTypes = {
|
|
724
|
+
NEW_ASSIGNMENT: "NEW_ASSIGNMENT",
|
|
725
|
+
FEEDBACK_FROM_TEACHER: "FEEDBACK_FROM_TEACHER",
|
|
726
|
+
MESSAGE_FROM_STUDENT: "MESSAGE_FROM_STUDENT",
|
|
727
|
+
PHRASE_MARKED_CORRECT: "PHRASE_MARKED_CORRECT",
|
|
728
|
+
STUDENT_PROGRESS: "STUDENT_PROGRESS",
|
|
729
|
+
PLAYLIST_FOLLOWERS: "PLAYLIST_FOLLOWERS",
|
|
730
|
+
PLAYLIST_PLAYS: "PLAYLIST_PLAYS",
|
|
731
|
+
// New notifications
|
|
732
|
+
ASSESSMENT_SUBMITTED: "ASSESSMENT_SUBMITTED",
|
|
733
|
+
// Notification FOR TEACHER when student submits assessment
|
|
734
|
+
ASSESSMENT_SCORED: "ASSESSMENT_SCORED",
|
|
735
|
+
// Notification FOR STUDENT when teacher scores assessment
|
|
736
|
+
// Comment
|
|
737
|
+
NEW_COMMENT: "NEW_COMMENT"
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
// src/domains/notification/services/create-notification.service.ts
|
|
741
|
+
import dayjs2 from "dayjs";
|
|
742
|
+
|
|
743
|
+
// src/constants/web.constants.ts
|
|
744
|
+
var WEB_BASE_URL = "https://app.speakable.io";
|
|
745
|
+
|
|
746
|
+
// src/lib/firebase/firebase-functions.ts
|
|
747
|
+
var SpeakableFirebaseFunctions = {
|
|
748
|
+
updateAssignmentGradebookStatus: api.httpsCallable("updateAssignmentGradebookStatus"),
|
|
749
|
+
onSetOpened: api.httpsCallable("onSetOpened"),
|
|
750
|
+
updateAlgoliaIndex: api.httpsCallable("updateAlgoliaIndex"),
|
|
751
|
+
submitAssignmentToGoogleClassroomV2: api.httpsCallable("submitAssignmentToGoogleClassroomV2"),
|
|
752
|
+
submitLTIAssignmentScore: api.httpsCallable("submitLTIAssignmentScore"),
|
|
753
|
+
submitAssignmentV2: api.httpsCallable("submitLTIAssignmentScoreV2"),
|
|
754
|
+
submitAssessment: api.httpsCallable("submitAssessment"),
|
|
755
|
+
sendAssessmentScoredEmail: api.httpsCallable("sendAssessmentScoredEmail"),
|
|
756
|
+
createNotification: api.httpsCallable("createNotificationV2")
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
// src/domains/notification/services/send-notification.service.ts
|
|
760
|
+
var _sendNotification = async (sendTo, notification) => {
|
|
761
|
+
const results = await SpeakableFirebaseFunctions.createNotification({
|
|
762
|
+
sendTo,
|
|
763
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
764
|
+
notification
|
|
765
|
+
});
|
|
766
|
+
return results;
|
|
767
|
+
};
|
|
768
|
+
var sendNotification = withErrorHandler(_sendNotification, "sendNotification");
|
|
769
|
+
|
|
770
|
+
// src/domains/notification/services/create-notification.service.ts
|
|
771
|
+
var createNotification = async ({
|
|
772
|
+
data,
|
|
773
|
+
type,
|
|
774
|
+
userId,
|
|
775
|
+
profile
|
|
776
|
+
}) => {
|
|
777
|
+
let result;
|
|
778
|
+
switch (type) {
|
|
779
|
+
case SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT:
|
|
780
|
+
result = await handleAssignNotifPromise({ data, profile });
|
|
781
|
+
break;
|
|
782
|
+
case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED:
|
|
783
|
+
result = await createAssessmentSubmissionNotification({
|
|
784
|
+
data,
|
|
785
|
+
profile,
|
|
786
|
+
userId
|
|
787
|
+
});
|
|
788
|
+
break;
|
|
789
|
+
case SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED:
|
|
790
|
+
result = await createAssessmentScoredNotification({
|
|
791
|
+
data,
|
|
792
|
+
profile
|
|
793
|
+
});
|
|
794
|
+
break;
|
|
795
|
+
default:
|
|
796
|
+
result = null;
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
return result;
|
|
800
|
+
};
|
|
801
|
+
var handleAssignNotifPromise = async ({
|
|
802
|
+
data: assignments,
|
|
803
|
+
profile
|
|
804
|
+
}) => {
|
|
805
|
+
if (!assignments.length) return;
|
|
806
|
+
try {
|
|
807
|
+
const notifsPromises = assignments.map(async (assignment) => {
|
|
808
|
+
const {
|
|
809
|
+
section,
|
|
810
|
+
section: { members },
|
|
811
|
+
...rest
|
|
812
|
+
} = assignment;
|
|
813
|
+
if (!section || !members) throw new Error("Invalid assignment data");
|
|
814
|
+
const data = { section, sendTo: members, assignment: { ...rest } };
|
|
815
|
+
return createNewAssignmentNotification({ data, profile });
|
|
816
|
+
});
|
|
817
|
+
await Promise.all(notifsPromises);
|
|
818
|
+
return {
|
|
819
|
+
success: true,
|
|
820
|
+
message: "Assignment notifications sent successfully"
|
|
821
|
+
};
|
|
822
|
+
} catch (error) {
|
|
823
|
+
console.error("Error in handleAssignNotifPromise:", error);
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
var createNewAssignmentNotification = async ({
|
|
828
|
+
data,
|
|
829
|
+
profile
|
|
830
|
+
}) => {
|
|
831
|
+
const { assignment, sendTo } = data;
|
|
832
|
+
const teacherName = profile.displayName || "Your teacher";
|
|
833
|
+
const dueDate = assignment.dueDateTimestamp ? dayjs2(assignment.dueDateTimestamp.toDate()).format("MMM Do") : null;
|
|
834
|
+
const results = await sendNotification(sendTo, {
|
|
835
|
+
courseId: assignment.courseId,
|
|
836
|
+
type: SPEAKABLE_NOTIFICATIONS.NEW_ASSIGNMENT,
|
|
837
|
+
senderName: teacherName,
|
|
838
|
+
link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
|
|
839
|
+
messagePreview: `A new assignment "${assignment.name}" is now available. ${dueDate ? `Due ${dueDate}` : ""}`,
|
|
840
|
+
title: "New Assignment Available!",
|
|
841
|
+
imageUrl: profile.image?.url
|
|
842
|
+
});
|
|
843
|
+
return results;
|
|
844
|
+
};
|
|
845
|
+
var createAssessmentSubmissionNotification = async ({
|
|
846
|
+
data: assignment,
|
|
847
|
+
profile,
|
|
848
|
+
userId
|
|
849
|
+
}) => {
|
|
850
|
+
const studentName = profile.displayName || "Your student";
|
|
851
|
+
const results = await sendNotification(assignment.owners, {
|
|
852
|
+
courseId: assignment.courseId,
|
|
853
|
+
type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SUBMITTED,
|
|
854
|
+
link: `${WEB_BASE_URL}/a/${assignment.id}?studentId=${userId}`,
|
|
855
|
+
title: `Assessment Submitted!`,
|
|
856
|
+
senderName: studentName,
|
|
857
|
+
messagePreview: `${studentName} has submitted the assessment "${assignment.name}"`,
|
|
858
|
+
imageUrl: profile.image?.url
|
|
859
|
+
});
|
|
860
|
+
return results;
|
|
861
|
+
};
|
|
862
|
+
var createAssessmentScoredNotification = async ({
|
|
863
|
+
data,
|
|
864
|
+
profile
|
|
865
|
+
}) => {
|
|
866
|
+
const { assignment, sendTo } = data;
|
|
867
|
+
const teacherName = profile.displayName || "Your teacher";
|
|
868
|
+
const title = `${assignment.isAssessment ? "Assessment" : "Assignment"} Reviewed!`;
|
|
869
|
+
const messagePreview = `Your ${assignment.isAssessment ? "assessment" : "assignment"} has been reviewed by your teacher. Click to view the feedback.`;
|
|
870
|
+
const results = await sendNotification(sendTo, {
|
|
871
|
+
courseId: assignment.courseId,
|
|
872
|
+
type: SPEAKABLE_NOTIFICATIONS.ASSESSMENT_SCORED,
|
|
873
|
+
link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
|
|
874
|
+
title,
|
|
875
|
+
messagePreview,
|
|
876
|
+
imageUrl: profile.image?.url,
|
|
877
|
+
senderName: teacherName
|
|
878
|
+
});
|
|
879
|
+
await SpeakableFirebaseFunctions.sendAssessmentScoredEmail({
|
|
880
|
+
assessmentTitle: assignment.name,
|
|
881
|
+
link: `${WEB_BASE_URL}/assignment/${assignment.id}`,
|
|
882
|
+
senderImage: profile.image?.url || "",
|
|
883
|
+
studentId: sendTo[0],
|
|
884
|
+
teacherName: profile.displayName
|
|
885
|
+
});
|
|
886
|
+
return results;
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/domains/notification/hooks/notification.hooks.ts
|
|
890
|
+
var notificationQueryKeys = {
|
|
891
|
+
all: ["notifications"],
|
|
892
|
+
byId: (id) => [...notificationQueryKeys.all, id]
|
|
893
|
+
};
|
|
894
|
+
var useCreateNotification = () => {
|
|
895
|
+
const { user, queryClient } = useSpeakableApi();
|
|
896
|
+
const handleCreateNotifications = async (type, data) => {
|
|
897
|
+
const result = await createNotification({
|
|
898
|
+
type,
|
|
899
|
+
userId: user?.auth.uid ?? "",
|
|
900
|
+
profile: user?.profile ?? {},
|
|
901
|
+
data
|
|
902
|
+
});
|
|
903
|
+
queryClient.invalidateQueries({
|
|
904
|
+
queryKey: notificationQueryKeys.byId(user?.auth.uid ?? "")
|
|
905
|
+
});
|
|
906
|
+
return result;
|
|
907
|
+
};
|
|
908
|
+
return {
|
|
909
|
+
createNotification: handleCreateNotifications
|
|
910
|
+
};
|
|
911
|
+
};
|
|
912
|
+
|
|
291
913
|
// src/lib/create-firebase-client.ts
|
|
292
|
-
|
|
914
|
+
import { httpsCallable as webHttpsCallable } from "firebase/functions";
|
|
915
|
+
import { httpsCallable as nativeHttpsCallable } from "@react-native-firebase/functions";
|
|
916
|
+
async function createFsClient({
|
|
917
|
+
db,
|
|
918
|
+
platform,
|
|
919
|
+
functions
|
|
920
|
+
}) {
|
|
293
921
|
const dbAsFirestore = db;
|
|
294
922
|
const helpers = platform === "web" ? await import("firebase/firestore") : await import("@react-native-firebase/firestore");
|
|
923
|
+
const httpsCallable = platform === "web" ? webHttpsCallable : nativeHttpsCallable;
|
|
295
924
|
api.initialize({
|
|
296
925
|
db: dbAsFirestore,
|
|
297
|
-
helpers
|
|
926
|
+
helpers,
|
|
927
|
+
functions,
|
|
928
|
+
httpsCallable
|
|
298
929
|
});
|
|
299
930
|
return {
|
|
300
|
-
assignmentRepo: createAssignmentRepo()
|
|
931
|
+
assignmentRepo: createAssignmentRepo(),
|
|
932
|
+
cardRepo: createCardRepo()
|
|
301
933
|
};
|
|
302
934
|
}
|
|
303
935
|
|
|
@@ -308,21 +940,33 @@ function SpeakableProvider({
|
|
|
308
940
|
db,
|
|
309
941
|
platform,
|
|
310
942
|
children,
|
|
311
|
-
queryClient
|
|
943
|
+
queryClient,
|
|
944
|
+
user,
|
|
945
|
+
permissions,
|
|
946
|
+
firebaseFunctions,
|
|
947
|
+
localStorage
|
|
312
948
|
}) {
|
|
313
949
|
const [speakableApi, setSpeakableApi] = useState(null);
|
|
314
950
|
useEffect(() => {
|
|
315
|
-
createFsClient(
|
|
951
|
+
createFsClient({
|
|
952
|
+
db,
|
|
953
|
+
platform,
|
|
954
|
+
functions: firebaseFunctions
|
|
955
|
+
}).then((repos) => {
|
|
316
956
|
setSpeakableApi(repos);
|
|
317
957
|
});
|
|
318
|
-
}, [db, platform]);
|
|
958
|
+
}, [db, firebaseFunctions, platform]);
|
|
319
959
|
if (!speakableApi) return null;
|
|
320
960
|
return /* @__PURE__ */ jsx(
|
|
321
961
|
FsCtx.Provider,
|
|
322
962
|
{
|
|
323
963
|
value: {
|
|
324
964
|
speakableApi,
|
|
325
|
-
queryClient
|
|
965
|
+
queryClient,
|
|
966
|
+
user,
|
|
967
|
+
permissions,
|
|
968
|
+
firebaseFunctions,
|
|
969
|
+
localStorage
|
|
326
970
|
},
|
|
327
971
|
children
|
|
328
972
|
}
|
|
@@ -334,11 +978,43 @@ function useSpeakableApi() {
|
|
|
334
978
|
return ctx;
|
|
335
979
|
}
|
|
336
980
|
export {
|
|
981
|
+
ALLOWED_CARD_ACTIVITY_TYPES_FOR_SUMMARY,
|
|
982
|
+
BASE_MULTIPLE_CHOICE_FIELD_VALUES,
|
|
983
|
+
BASE_REPEAT_FIELD_VALUES,
|
|
984
|
+
BASE_RESPOND_FIELD_VALUES,
|
|
985
|
+
CardActivityType,
|
|
986
|
+
FeedbackTypesCard,
|
|
337
987
|
FsCtx,
|
|
988
|
+
LENIENCY_OPTIONS,
|
|
989
|
+
LeniencyCard,
|
|
990
|
+
MULTIPLE_CHOICE_CARD_ACTIVITY_TYPES,
|
|
991
|
+
REPEAT_CARD_ACTIVITY_TYPES,
|
|
992
|
+
RESPOND_AUDIO_CARD_ACTIVITY_TYPES,
|
|
993
|
+
RESPOND_CARD_ACTIVITY_TYPES,
|
|
994
|
+
RESPOND_WRITE_CARD_ACTIVITY_TYPES,
|
|
995
|
+
SPEAKABLE_NOTIFICATIONS,
|
|
996
|
+
STUDENT_LEVELS_OPTIONS,
|
|
997
|
+
SpeakableNotificationTypes,
|
|
338
998
|
SpeakableProvider,
|
|
999
|
+
VerificationCardStatus,
|
|
339
1000
|
assignmentQueryKeys,
|
|
1001
|
+
cardsQueryKeys,
|
|
340
1002
|
createAssignmentRepo,
|
|
1003
|
+
createCardRepo,
|
|
341
1004
|
createFsClient,
|
|
1005
|
+
createSetRepo,
|
|
1006
|
+
getCardFromCache,
|
|
1007
|
+
getSetFromCache,
|
|
1008
|
+
refsCardsFiresotre,
|
|
1009
|
+
refsSetsFirestore,
|
|
1010
|
+
setsQueryKeys,
|
|
1011
|
+
updateCardInCache,
|
|
1012
|
+
updateSetInCache,
|
|
342
1013
|
useAssignment,
|
|
1014
|
+
useCards,
|
|
1015
|
+
useCreateCard,
|
|
1016
|
+
useCreateCards,
|
|
1017
|
+
useCreateNotification,
|
|
1018
|
+
useSet,
|
|
343
1019
|
useSpeakableApi
|
|
344
1020
|
};
|