musora-content-services 2.123.2 → 2.124.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/.claude/settings.local.json +9 -0
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +37 -0
- package/src/index.d.ts +8 -0
- package/src/index.js +8 -0
- package/src/services/awards/award-query.js +7 -0
- package/src/services/content-org/learning-paths.ts +78 -11
- package/src/services/contentProgress.js +6 -0
- package/src/services/progress-row/base.js +2 -0
- package/src/services/progress-row/rows/content-card.js +7 -1
- package/src/services/progress-row/rows/playlist-card.js +4 -1
- package/src/services/sanity.js +15 -2
- package/src/services/sync/repositories/content-progress.ts +28 -2
- package/src/services/urlBuilder.ts +3 -1
- package/src/services/userActivity.js +16 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.124.0](https://github.com/railroadmedia/musora-content-services/compare/v2.123.2...v2.124.0) (2026-01-29)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEH-1505:** new award templates ([#740](https://github.com/railroadmedia/musora-content-services/issues/740)) ([e11f5f0](https://github.com/railroadmedia/musora-content-services/commit/e11f5f0db218132409b1d228a4c0a20500f73922))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **T3PS-1715:** lp lesson practices and activity mapping ([#732](https://github.com/railroadmedia/musora-content-services/issues/732)) ([922e455](https://github.com/railroadmedia/musora-content-services/commit/922e4558b720b17533b3923fd08939ef69afa4f2))
|
|
16
|
+
|
|
5
17
|
### [2.123.2](https://github.com/railroadmedia/musora-content-services/compare/v2.123.1...v2.123.2) (2026-01-28)
|
|
6
18
|
|
|
7
19
|
|
package/package.json
CHANGED
package/src/contentTypeConfig.js
CHANGED
|
@@ -20,6 +20,8 @@ export const SONG_TYPES_WITH_CHILDREN = [
|
|
|
20
20
|
// Single hierarchy refers to only one element in the hierarchy has video lessons, not that they have a single parent
|
|
21
21
|
export const SINGLE_PARENT_TYPES = ['course-lesson', 'pack-bundle-lesson', 'song-tutorial-lesson']
|
|
22
22
|
|
|
23
|
+
export const LEARNING_PATH_LESSON = 'learning-path-lesson-v2'
|
|
24
|
+
|
|
23
25
|
export const genreField = `genre[]->{
|
|
24
26
|
name,
|
|
25
27
|
'slug': slug.current,
|
|
@@ -387,6 +389,7 @@ export let contentTypeConfig = {
|
|
|
387
389
|
fields: [
|
|
388
390
|
'"parent_content_data": parent_content_data[].id',
|
|
389
391
|
'"badge" : *[references(^._id) && _type == "content-award"][0].badge.asset->url',
|
|
392
|
+
'"badge_logo" : *[references(^._id) && _type == "content-award"][0].logo.asset->url',
|
|
390
393
|
],
|
|
391
394
|
includeChildFields: true,
|
|
392
395
|
},
|
|
@@ -986,3 +989,37 @@ export const getFormattedType = (type, brand) => {
|
|
|
986
989
|
|
|
987
990
|
return null
|
|
988
991
|
}
|
|
992
|
+
|
|
993
|
+
export const awardTemplate = {
|
|
994
|
+
drumeo: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/drumeo.svg",
|
|
995
|
+
guitareo: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/guitareo.svg",
|
|
996
|
+
pianote: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/pianote.svg",
|
|
997
|
+
singeo: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/singeo.svg",
|
|
998
|
+
playbass: "https://d3fzm1tzeyr5n3.cloudfront.net/v2/awards/playbass.svg",
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Adds award badge_template to content(s) where badge_logo exists
|
|
1003
|
+
* @param {object|object[]} content - sanity content(s) response
|
|
1004
|
+
* @param {string} brand - brand for if content brand is missing
|
|
1005
|
+
* @returns {object|object[]} post-processed content
|
|
1006
|
+
*/
|
|
1007
|
+
export function addAwardTemplateToContent(content, brand= null) {
|
|
1008
|
+
if (!content) return content
|
|
1009
|
+
|
|
1010
|
+
// should be fine with this; children don't need awards.
|
|
1011
|
+
// assumes if badge_logo exists, it needs a badge_template.
|
|
1012
|
+
if (Array.isArray(content)) {
|
|
1013
|
+
content.forEach((item) => {
|
|
1014
|
+
if (item['badge_logo'] && !item['badge_template']) {
|
|
1015
|
+
item['badge_template'] = awardTemplate[item['brand'] || brand]
|
|
1016
|
+
}
|
|
1017
|
+
})
|
|
1018
|
+
} else {
|
|
1019
|
+
if (content['badge_logo'] && !content['badge_template']) {
|
|
1020
|
+
content['badge_template'] = awardTemplate[content['brand'] || brand]
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return content
|
|
1025
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -55,6 +55,8 @@ import {
|
|
|
55
55
|
getEnrichedLearningPaths,
|
|
56
56
|
getLearningPathLessonsByIds,
|
|
57
57
|
mapContentToParent,
|
|
58
|
+
mapContentsThatWereLastProgressedFromMethod,
|
|
59
|
+
mapLearningPathParentsTo,
|
|
58
60
|
onContentCompletedLearningPathActions,
|
|
59
61
|
resetAllLearningPaths,
|
|
60
62
|
startLearningPath,
|
|
@@ -114,6 +116,7 @@ import {
|
|
|
114
116
|
getAllCompletedByIds,
|
|
115
117
|
getAllStarted,
|
|
116
118
|
getAllStartedOrCompleted,
|
|
119
|
+
getIdsWhereLastAccessedFromMethod,
|
|
117
120
|
getLastInteractedOf,
|
|
118
121
|
getNavigateTo,
|
|
119
122
|
getNavigateToForMethod,
|
|
@@ -286,6 +289,7 @@ import {
|
|
|
286
289
|
fetchOtherSongVersions,
|
|
287
290
|
fetchOwnedContent,
|
|
288
291
|
fetchPackData,
|
|
292
|
+
fetchParentChildRelationshipsFor,
|
|
289
293
|
fetchPlayAlongsCount,
|
|
290
294
|
fetchRecent,
|
|
291
295
|
fetchRelatedLessons,
|
|
@@ -540,6 +544,7 @@ declare module 'musora-content-services' {
|
|
|
540
544
|
fetchOtherSongVersions,
|
|
541
545
|
fetchOwnedContent,
|
|
542
546
|
fetchPackData,
|
|
547
|
+
fetchParentChildRelationshipsFor,
|
|
543
548
|
fetchPlayAlongsCount,
|
|
544
549
|
fetchPlaylist,
|
|
545
550
|
fetchPlaylistItems,
|
|
@@ -599,6 +604,7 @@ declare module 'musora-content-services' {
|
|
|
599
604
|
getDailySession,
|
|
600
605
|
getEnrichedLearningPath,
|
|
601
606
|
getEnrichedLearningPaths,
|
|
607
|
+
getIdsWhereLastAccessedFromMethod,
|
|
602
608
|
getInProgressAwards,
|
|
603
609
|
getLastInteractedOf,
|
|
604
610
|
getLearningPathLessonsByIds,
|
|
@@ -659,6 +665,8 @@ declare module 'musora-content-services' {
|
|
|
659
665
|
login,
|
|
660
666
|
logout,
|
|
661
667
|
mapContentToParent,
|
|
668
|
+
mapContentsThatWereLastProgressedFromMethod,
|
|
669
|
+
mapLearningPathParentsTo,
|
|
662
670
|
markAllNotificationsAsRead,
|
|
663
671
|
markContentAsInterested,
|
|
664
672
|
markContentAsNotInterested,
|
package/src/index.js
CHANGED
|
@@ -59,6 +59,8 @@ import {
|
|
|
59
59
|
getEnrichedLearningPaths,
|
|
60
60
|
getLearningPathLessonsByIds,
|
|
61
61
|
mapContentToParent,
|
|
62
|
+
mapContentsThatWereLastProgressedFromMethod,
|
|
63
|
+
mapLearningPathParentsTo,
|
|
62
64
|
onContentCompletedLearningPathActions,
|
|
63
65
|
resetAllLearningPaths,
|
|
64
66
|
startLearningPath,
|
|
@@ -118,6 +120,7 @@ import {
|
|
|
118
120
|
getAllCompletedByIds,
|
|
119
121
|
getAllStarted,
|
|
120
122
|
getAllStartedOrCompleted,
|
|
123
|
+
getIdsWhereLastAccessedFromMethod,
|
|
121
124
|
getLastInteractedOf,
|
|
122
125
|
getNavigateTo,
|
|
123
126
|
getNavigateToForMethod,
|
|
@@ -290,6 +293,7 @@ import {
|
|
|
290
293
|
fetchOtherSongVersions,
|
|
291
294
|
fetchOwnedContent,
|
|
292
295
|
fetchPackData,
|
|
296
|
+
fetchParentChildRelationshipsFor,
|
|
293
297
|
fetchPlayAlongsCount,
|
|
294
298
|
fetchRecent,
|
|
295
299
|
fetchRelatedLessons,
|
|
@@ -539,6 +543,7 @@ export {
|
|
|
539
543
|
fetchOtherSongVersions,
|
|
540
544
|
fetchOwnedContent,
|
|
541
545
|
fetchPackData,
|
|
546
|
+
fetchParentChildRelationshipsFor,
|
|
542
547
|
fetchPlayAlongsCount,
|
|
543
548
|
fetchPlaylist,
|
|
544
549
|
fetchPlaylistItems,
|
|
@@ -598,6 +603,7 @@ export {
|
|
|
598
603
|
getDailySession,
|
|
599
604
|
getEnrichedLearningPath,
|
|
600
605
|
getEnrichedLearningPaths,
|
|
606
|
+
getIdsWhereLastAccessedFromMethod,
|
|
601
607
|
getInProgressAwards,
|
|
602
608
|
getLastInteractedOf,
|
|
603
609
|
getLearningPathLessonsByIds,
|
|
@@ -658,6 +664,8 @@ export {
|
|
|
658
664
|
login,
|
|
659
665
|
logout,
|
|
660
666
|
mapContentToParent,
|
|
667
|
+
mapContentsThatWereLastProgressedFromMethod,
|
|
668
|
+
mapLearningPathParentsTo,
|
|
661
669
|
markAllNotificationsAsRead,
|
|
662
670
|
markContentAsInterested,
|
|
663
671
|
markContentAsNotInterested,
|
|
@@ -57,6 +57,7 @@ import { awardDefinitions } from './internal/award-definitions'
|
|
|
57
57
|
import { AwardMessageGenerator } from './internal/message-generator'
|
|
58
58
|
import db from '../sync/repository-proxy'
|
|
59
59
|
import UserAwardProgressRepository from '../sync/repositories/user-award-progress'
|
|
60
|
+
import {awardTemplate} from "../../contentTypeConfig.js";
|
|
60
61
|
|
|
61
62
|
function enhanceCompletionData(completionData) {
|
|
62
63
|
if (!completionData) return null
|
|
@@ -222,7 +223,9 @@ function defineAwards(data) {
|
|
|
222
223
|
return {
|
|
223
224
|
awardId: def._id,
|
|
224
225
|
awardTitle: def.name,
|
|
226
|
+
logo: def.logo,
|
|
225
227
|
badge: def.badge,
|
|
228
|
+
badge_template: awardTemplate[def.brand],
|
|
226
229
|
award: def.award,
|
|
227
230
|
brand: def.brand,
|
|
228
231
|
instructorName: def.instructor_name,
|
|
@@ -324,7 +327,9 @@ export async function getCompletedAwards(brand = null, options = {}) {
|
|
|
324
327
|
awardId: progress.award_id,
|
|
325
328
|
awardTitle: definition.name,
|
|
326
329
|
awardType: definition.type,
|
|
330
|
+
logo: definition.logo,
|
|
327
331
|
badge: definition.badge,
|
|
332
|
+
badge_template: awardTemplate[definition.brand],
|
|
328
333
|
award: definition.award,
|
|
329
334
|
brand: definition.brand,
|
|
330
335
|
hasCertificate: hasCertificate,
|
|
@@ -445,7 +450,9 @@ export async function getInProgressAwards(brand = null, options = {}) {
|
|
|
445
450
|
return {
|
|
446
451
|
awardId: progress.award_id,
|
|
447
452
|
awardTitle: definition.name,
|
|
453
|
+
logo: definition.logo,
|
|
448
454
|
badge: definition.badge,
|
|
455
|
+
badge_template: awardTemplate[definition.brand],
|
|
449
456
|
award: definition.award,
|
|
450
457
|
brand: definition.brand,
|
|
451
458
|
instructorName: definition.instructor_name,
|
|
@@ -3,13 +3,19 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { GET, POST } from '../../infrastructure/http/HttpClient.ts'
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
fetchByRailContentId,
|
|
8
|
+
fetchByRailContentIds,
|
|
9
|
+
fetchMethodV2Structure,
|
|
10
|
+
fetchParentChildRelationshipsFor
|
|
11
|
+
} from '../sanity.js'
|
|
7
12
|
import { addContextToLearningPaths } from '../contentAggregator.js'
|
|
8
13
|
import {
|
|
9
14
|
contentStatusCompleted,
|
|
10
15
|
contentStatusCompletedMany,
|
|
11
16
|
contentStatusReset,
|
|
12
17
|
getAllCompletedByIds,
|
|
18
|
+
getIdsWhereLastAccessedFromMethod,
|
|
13
19
|
getProgressState,
|
|
14
20
|
} from '../contentProgress.js'
|
|
15
21
|
import { COLLECTION_TYPE, STATE } from '../sync/models/ContentProgress'
|
|
@@ -17,10 +23,10 @@ import { SyncWriteDTO } from '../sync'
|
|
|
17
23
|
import { ContentProgress } from '../sync/models'
|
|
18
24
|
import { CollectionParameter } from '../sync/repositories/content-progress'
|
|
19
25
|
import dayjs from 'dayjs'
|
|
26
|
+
import { LEARNING_PATH_LESSON } from "../../contentTypeConfig";
|
|
20
27
|
|
|
21
28
|
const BASE_PATH: string = `/api/content-org`
|
|
22
29
|
const LEARNING_PATHS_PATH = `${BASE_PATH}/v1/user/learning-paths`
|
|
23
|
-
const LEARNING_PATH_LESSON = 'learning-path-lesson-v2'
|
|
24
30
|
let dailySessionPromise: Promise<DailySessionResponse> | null = null
|
|
25
31
|
let activePathPromise: Promise<ActiveLearningPathResponse> | null = null
|
|
26
32
|
|
|
@@ -202,7 +208,10 @@ export async function getEnrichedLearningPath(learningPathId) {
|
|
|
202
208
|
response = await addContextToLearningPaths(() => response, { addAwards: true })
|
|
203
209
|
if (!response) return response
|
|
204
210
|
|
|
205
|
-
response.children = mapContentToParent(
|
|
211
|
+
response.children = mapContentToParent(
|
|
212
|
+
response.children,
|
|
213
|
+
{lessonType: LEARNING_PATH_LESSON, parentContentId: learningPathId}
|
|
214
|
+
)
|
|
206
215
|
return response
|
|
207
216
|
}
|
|
208
217
|
|
|
@@ -234,9 +243,8 @@ export async function getEnrichedLearningPaths(learningPathIds: number[]) {
|
|
|
234
243
|
|
|
235
244
|
response.forEach((learningPath) => {
|
|
236
245
|
learningPath.children = mapContentToParent(
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
learningPath.id
|
|
246
|
+
learningPath.children,
|
|
247
|
+
{lessonType: LEARNING_PATH_LESSON, parentContentId: learningPath.id}
|
|
240
248
|
)
|
|
241
249
|
})
|
|
242
250
|
return response
|
|
@@ -257,15 +265,32 @@ export async function getLearningPathLessonsByIds(contentIds, learningPathId) {
|
|
|
257
265
|
|
|
258
266
|
/**
|
|
259
267
|
* Maps content to its parent learning path - fixes multi-parent problems for cta when lessons have a special collection.
|
|
260
|
-
* @param lessons
|
|
268
|
+
* @param lessons - sanity documents
|
|
261
269
|
* @param parentContentType
|
|
262
270
|
* @param parentContentId
|
|
263
271
|
*/
|
|
264
|
-
export function mapContentToParent(
|
|
272
|
+
export function mapContentToParent(
|
|
273
|
+
lessons: any,
|
|
274
|
+
options?: { lessonType?: string; parentContentId?: number }
|
|
275
|
+
) {
|
|
265
276
|
if (!lessons) return lessons
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
277
|
+
|
|
278
|
+
function mapIt(lesson: any) {
|
|
279
|
+
const mappedLesson = { ...lesson }
|
|
280
|
+
if (options?.lessonType !== undefined) mappedLesson.type = options.lessonType
|
|
281
|
+
if (options?.parentContentId !== undefined) mappedLesson.parent_id = options.parentContentId
|
|
282
|
+
return mappedLesson
|
|
283
|
+
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (typeof lessons === 'object' && !Array.isArray(lessons)) {
|
|
287
|
+
return mapIt(lessons)
|
|
288
|
+
|
|
289
|
+
} else if (Array.isArray(lessons)) {
|
|
290
|
+
return lessons.map((lesson: any) => {
|
|
291
|
+
return mapIt(lesson)
|
|
292
|
+
})
|
|
293
|
+
}
|
|
269
294
|
}
|
|
270
295
|
|
|
271
296
|
interface fetchLearningPathLessonsResponse {
|
|
@@ -539,3 +564,45 @@ export async function onContentCompletedLearningPathActions(
|
|
|
539
564
|
|
|
540
565
|
await contentStatusReset(nextLearningPathData.intro_video.id, { skipPush: true })
|
|
541
566
|
}
|
|
567
|
+
|
|
568
|
+
export async function mapContentsThatWereLastProgressedFromMethod(objects: any[]) {
|
|
569
|
+
if (!objects || objects.length === 0) return objects
|
|
570
|
+
|
|
571
|
+
const contentIds = objects.map((obj) => obj.id) as number[]
|
|
572
|
+
const trueIds = await getIdsWhereLastAccessedFromMethod(contentIds)
|
|
573
|
+
|
|
574
|
+
if (trueIds.length === 0) return objects
|
|
575
|
+
|
|
576
|
+
let filtered = objects.filter((obj) => trueIds.includes(obj.id))
|
|
577
|
+
|
|
578
|
+
filtered = await mapLearningPathParentsTo(filtered, {type: true, parent_id: true})
|
|
579
|
+
|
|
580
|
+
// Map each filtered item back into the total contents object
|
|
581
|
+
objects = objects.map((item) => {
|
|
582
|
+
const replace = filtered.find((f) => f.id === item.id) || item
|
|
583
|
+
return replace
|
|
584
|
+
})
|
|
585
|
+
|
|
586
|
+
return objects
|
|
587
|
+
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
export async function mapLearningPathParentsTo(objects: any[], fieldsToMap?: {type?: boolean, parent_id?: boolean}): Promise<object[]> {
|
|
591
|
+
const ids = objects.map((obj: any) => obj.id) as number[]
|
|
592
|
+
const hierarchy = await fetchParentChildRelationshipsFor(ids, COLLECTION_TYPE.LEARNING_PATH)
|
|
593
|
+
|
|
594
|
+
const parentMap = new Map<number, number>()
|
|
595
|
+
hierarchy.forEach((relation) => {
|
|
596
|
+
relation.children.forEach((childId) => {
|
|
597
|
+
parentMap.set(childId, Number(relation.railcontent_id))
|
|
598
|
+
})
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
return objects.map((obj) => {
|
|
602
|
+
const parent_id = parentMap.get(obj.id) ?? undefined
|
|
603
|
+
return mapContentToParent(obj, {
|
|
604
|
+
lessonType: fieldsToMap.type ? LEARNING_PATH_LESSON : undefined,
|
|
605
|
+
parentContentId: fieldsToMap.parent_id ? parent_id : undefined
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
}
|
|
@@ -797,3 +797,9 @@ function normalizeCollection(collection) {
|
|
|
797
797
|
id: typeof collection.id === 'string' ? +collection.id : collection.id,
|
|
798
798
|
}
|
|
799
799
|
}
|
|
800
|
+
|
|
801
|
+
export async function getIdsWhereLastAccessedFromMethod(contentIds) {
|
|
802
|
+
const records = await db.contentProgress.getSomeProgressWhereLastAccessedFromMethod(normalizeContentIds(contentIds))
|
|
803
|
+
|
|
804
|
+
return records.data.map(record => record.content_id)
|
|
805
|
+
}
|
|
@@ -15,6 +15,7 @@ import { addContextToContent } from '../contentAggregator.js'
|
|
|
15
15
|
import { fetchPlaylist } from '../content-org/playlists.js'
|
|
16
16
|
import { TabResponseType } from '../../contentMetaData.js'
|
|
17
17
|
import { PUT } from '../../infrastructure/http/HttpClient.ts'
|
|
18
|
+
import { addAwardTemplateToContent } from "../../contentTypeConfig.js";
|
|
18
19
|
|
|
19
20
|
export const USER_PIN_PROGRESS_KEY = 'user_pin_progress_row'
|
|
20
21
|
|
|
@@ -141,6 +142,7 @@ async function popPinnedItem(userPinnedItem, contentCardMap, playlistCards, meth
|
|
|
141
142
|
} else {
|
|
142
143
|
// we use fetchByRailContentIds so that we don't have the _type restriction in the query
|
|
143
144
|
let data = await fetchByRailContentIds([pinnedId], 'progress-tracker')
|
|
145
|
+
data = addAwardTemplateToContent(data)
|
|
144
146
|
item = await processContentItem(
|
|
145
147
|
await addContextToContent(() => data[0] ?? null, {
|
|
146
148
|
addNextLesson: true,
|
|
@@ -5,6 +5,8 @@ import { getAllStartedOrCompleted, getProgressStateByIds } from '../../contentPr
|
|
|
5
5
|
import { addContextToContent } from '../../contentAggregator.js'
|
|
6
6
|
import { fetchByRailContentIds, fetchShows } from '../../sanity.js'
|
|
7
7
|
import {
|
|
8
|
+
addAwardTemplateToContent,
|
|
9
|
+
awardTemplate,
|
|
8
10
|
collectionLessonTypes,
|
|
9
11
|
getFormattedType,
|
|
10
12
|
recentTypes,
|
|
@@ -30,7 +32,7 @@ export async function getContentCardMap(brand, limit, playlistEngagedOnContent,
|
|
|
30
32
|
recentContentIds = recentContentIds.filter(id => id !== item.id && !parentIds.includes(id))
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
|
-
|
|
35
|
+
let contents = recentContentIds.length > 0
|
|
34
36
|
? await addContextToContent(
|
|
35
37
|
fetchByRailContentIds,
|
|
36
38
|
recentContentIds,
|
|
@@ -44,6 +46,8 @@ export async function getContentCardMap(brand, limit, playlistEngagedOnContent,
|
|
|
44
46
|
}
|
|
45
47
|
)
|
|
46
48
|
: []
|
|
49
|
+
contents = addAwardTemplateToContent(contents)
|
|
50
|
+
|
|
47
51
|
const contentCards = await Promise.all(generateContentPromises(contents))
|
|
48
52
|
return contentCards.reduce((contentMap, content) => {
|
|
49
53
|
contentMap.set(content.id, content)
|
|
@@ -111,7 +115,9 @@ export async function processContentItem(content) {
|
|
|
111
115
|
thumbnail: content.thumbnail,
|
|
112
116
|
title: content.title,
|
|
113
117
|
isLive: isLive,
|
|
118
|
+
badge_logo: content.logo ?? null,
|
|
114
119
|
badge: content.badge ?? null,
|
|
120
|
+
badge_template: awardTemplate[content.brand],
|
|
115
121
|
isLocked: content.is_locked ?? false,
|
|
116
122
|
subtitle:
|
|
117
123
|
collectionLessonTypes.includes(content.type) || content.lesson_count > 1
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { fetchUserPlaylists } from '../../content-org/playlists.js'
|
|
5
5
|
import { addContextToContent } from '../../contentAggregator.js'
|
|
6
6
|
import { fetchByRailContentIds } from '../../sanity.js'
|
|
7
|
+
import { addAwardTemplateToContent } from "../../../contentTypeConfig.js";
|
|
7
8
|
|
|
8
9
|
export async function getPlaylistCards(recentPlaylists){
|
|
9
10
|
return await Promise.all(
|
|
@@ -65,7 +66,7 @@ export async function getPlaylistEngagedOnContent(recentPlaylists){
|
|
|
65
66
|
const playlistEngagedOnContents = recentPlaylists.map(
|
|
66
67
|
(item) => item.playlist.last_engaged_on
|
|
67
68
|
)
|
|
68
|
-
|
|
69
|
+
let contents = playlistEngagedOnContents.length > 0
|
|
69
70
|
? await addContextToContent(fetchByRailContentIds, playlistEngagedOnContents, 'progress-tracker', {
|
|
70
71
|
addNavigateTo: true,
|
|
71
72
|
addProgressStatus: true,
|
|
@@ -73,4 +74,6 @@ export async function getPlaylistEngagedOnContent(recentPlaylists){
|
|
|
73
74
|
addProgressTimestamp: true,
|
|
74
75
|
})
|
|
75
76
|
: []
|
|
77
|
+
contents = addAwardTemplateToContent(contents)
|
|
78
|
+
return contents
|
|
76
79
|
}
|
package/src/services/sanity.js
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
showsTypes,
|
|
33
33
|
SONG_TYPES,
|
|
34
34
|
SONG_TYPES_WITH_CHILDREN,
|
|
35
|
-
liveFields,
|
|
35
|
+
liveFields, awardTemplate, addAwardTemplateToContent,
|
|
36
36
|
} from '../contentTypeConfig.js'
|
|
37
37
|
import { fetchSimilarItems, recommendations } from './recommendations.js'
|
|
38
38
|
import { getSongType, processMetadata, ALWAYS_VISIBLE_TABS, CONTENT_STATUSES } from '../contentMetaData.js'
|
|
@@ -944,6 +944,7 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
|
|
|
944
944
|
"dark_mode_logo": dark_mode_logo_url.asset->url,
|
|
945
945
|
"light_mode_logo": light_mode_logo_url.asset->url,
|
|
946
946
|
"badge": *[references(^._id) && _type == 'content-award'][0].badge.asset->url,
|
|
947
|
+
"badge_logo": *[references(^._id) && _type == 'content-award'][0].logo.asset->url,
|
|
947
948
|
},`
|
|
948
949
|
: ''
|
|
949
950
|
|
|
@@ -994,7 +995,10 @@ export async function fetchLessonContent(railContentId, { addParent = false } =
|
|
|
994
995
|
return result
|
|
995
996
|
}
|
|
996
997
|
|
|
997
|
-
|
|
998
|
+
let contents = await fetchSanity(query, false, { customPostProcess: chapterProcess, processNeedAccess: true })
|
|
999
|
+
contents = addAwardTemplateToContent(contents)
|
|
1000
|
+
|
|
1001
|
+
return contents
|
|
998
1002
|
}
|
|
999
1003
|
|
|
1000
1004
|
/**
|
|
@@ -2332,3 +2336,12 @@ function getContentTypesForFilterName(displayName) {
|
|
|
2332
2336
|
export function getSongTypesFor(brand) {
|
|
2333
2337
|
return getSongType(brand)
|
|
2334
2338
|
}
|
|
2339
|
+
|
|
2340
|
+
export function fetchParentChildRelationshipsFor(childIds, parentType) {
|
|
2341
|
+
const stringIds = childIds.join(',')
|
|
2342
|
+
const query = `*[_type == '${parentType}' && count(@.child[@->railcontent_id in [${stringIds}]]) > 0]{
|
|
2343
|
+
railcontent_id,
|
|
2344
|
+
"children": child[@->railcontent_id in [${stringIds}]]->railcontent_id
|
|
2345
|
+
}`
|
|
2346
|
+
return fetchSanity(query, true)
|
|
2347
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import SyncRepository, {Q} from './base'
|
|
2
2
|
import ContentProgress, {COLLECTION_ID_SELF, COLLECTION_TYPE, STATE} from '../models/ContentProgress'
|
|
3
|
+
import {EpochMs} from "../index";
|
|
3
4
|
|
|
4
5
|
interface ContentIdCollectionTuple {
|
|
5
6
|
contentId: number,
|
|
@@ -123,6 +124,31 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
123
124
|
return await this.queryAll(...clauses)
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
// Two ways of checking this for a given content_id:
|
|
128
|
+
// * grab both records (collection_type = self & and collection_type = learning-path-v2), and compare their updated_at timestamps.
|
|
129
|
+
// * utilize the new last_interacted_a_la_carte, which is updated whenever the content is accessed OUTSIDE of an LP, and compare THIS with the self updated_at (which will be greater than if it was last accessed from LP)
|
|
130
|
+
// I went with the second because it's an easier query
|
|
131
|
+
async getSomeProgressWhereLastAccessedFromMethod(contentIds: number[]) {
|
|
132
|
+
const clauses = [
|
|
133
|
+
Q.where('content_id', Q.oneOf(contentIds)),
|
|
134
|
+
Q.where('collection_type', COLLECTION_TYPE.SELF),
|
|
135
|
+
Q.where('collection_id', COLLECTION_ID_SELF),
|
|
136
|
+
Q.or(
|
|
137
|
+
Q.and(
|
|
138
|
+
Q.where('updated_at', Q.notEq(null)),
|
|
139
|
+
Q.where('last_interacted_a_la_carte', null)
|
|
140
|
+
),
|
|
141
|
+
Q.and(
|
|
142
|
+
Q.where('updated_at', Q.notEq(null)),
|
|
143
|
+
Q.where('last_interacted_a_la_carte', Q.notEq(null)),
|
|
144
|
+
Q.where('updated_at', Q.gt(Q.column('last_interacted_a_la_carte')))
|
|
145
|
+
)
|
|
146
|
+
)
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
return await this.queryAll(...clauses)
|
|
150
|
+
}
|
|
151
|
+
|
|
126
152
|
async getSomeProgressByContentIdsAndCollections(tuples: ContentIdCollectionTuple[]) {
|
|
127
153
|
const clauses = []
|
|
128
154
|
|
|
@@ -160,7 +186,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
160
186
|
}
|
|
161
187
|
|
|
162
188
|
if (!fromLearningPath) {
|
|
163
|
-
r.last_interacted_a_la_carte = Date.now()
|
|
189
|
+
r.last_interacted_a_la_carte = Date.now() as EpochMs
|
|
164
190
|
}
|
|
165
191
|
|
|
166
192
|
}, { skipPush })
|
|
@@ -210,7 +236,7 @@ export default class ProgressRepository extends SyncRepository<ContentProgress>
|
|
|
210
236
|
r.progress_percent = progressPct
|
|
211
237
|
|
|
212
238
|
if (!fromLearningPath) {
|
|
213
|
-
r.last_interacted_a_la_carte = Date.now()
|
|
239
|
+
r.last_interacted_a_la_carte = Date.now() as EpochMs
|
|
214
240
|
}
|
|
215
241
|
},
|
|
216
242
|
])
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import { globalConfig } from './config.js'
|
|
16
16
|
import { Brands } from '../lib/brands.js'
|
|
17
|
+
import { LEARNING_PATH_LESSON } from '../contentTypeConfig.js'
|
|
18
|
+
import { COLLECTION_TYPE } from "./sync/models/ContentProgress";
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* Brand type - accepts enum values or string
|
|
@@ -149,7 +151,7 @@ export function generateContentUrl({
|
|
|
149
151
|
'jam-track',
|
|
150
152
|
]
|
|
151
153
|
|
|
152
|
-
const methodTypes = [
|
|
154
|
+
const methodTypes = [COLLECTION_TYPE.LEARNING_PATH, LEARNING_PATH_LESSON]
|
|
153
155
|
|
|
154
156
|
let pageType: string
|
|
155
157
|
if (songTypes.includes(type)) {
|
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
import { fetchUserPractices, fetchUserPracticeMeta, fetchRecentUserActivities } from './railcontent'
|
|
6
6
|
import { GET, POST, PUT, DELETE } from '../infrastructure/http/HttpClient.ts'
|
|
7
7
|
import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
8
|
-
import { fetchByRailContentIds } from './sanity'
|
|
8
|
+
import { fetchByRailContentIds, fetchParentChildRelationshipsFor } from './sanity'
|
|
9
9
|
import { getMonday, getWeekNumber, isSameDate, isNextDay } from './dateUtils.js'
|
|
10
10
|
import { globalConfig } from './config'
|
|
11
|
-
import { getFormattedType } from '../contentTypeConfig'
|
|
11
|
+
import { addAwardTemplateToContent, getFormattedType, LEARNING_PATH_LESSON } from '../contentTypeConfig'
|
|
12
12
|
import dayjs from 'dayjs'
|
|
13
13
|
import { addContextToContent } from './contentAggregator.js'
|
|
14
14
|
import { db, Q } from './sync'
|
|
15
15
|
import { COLLECTION_TYPE } from './sync/models/ContentProgress'
|
|
16
16
|
import { streakCalculator } from './user/streakCalculator'
|
|
17
|
+
import { mapContentsThatWereLastProgressedFromMethod, mapLearningPathParentsTo } from "./content-org/learning-paths.js";
|
|
17
18
|
|
|
18
19
|
const DATA_KEY_PRACTICES = 'practices'
|
|
19
20
|
|
|
@@ -490,11 +491,13 @@ export async function getPracticeNotes(date) {
|
|
|
490
491
|
*/
|
|
491
492
|
export async function getRecentActivity({ page = 1, limit = 5, tabName = null } = {}) {
|
|
492
493
|
const recentActivityData = await fetchRecentUserActivities({ page, limit, tabName })
|
|
493
|
-
const contentIds = recentActivityData.data.map((p) => p.contentId).filter((id) => id !== null)
|
|
494
494
|
|
|
495
|
-
const
|
|
495
|
+
const filteredData = recentActivityData.data.filter((id) => id !== null)
|
|
496
|
+
const allContentIds = filteredData.map((p) => p.contentId)
|
|
497
|
+
|
|
498
|
+
let contents = await addContextToContent(
|
|
496
499
|
fetchByRailContentIds,
|
|
497
|
-
|
|
500
|
+
allContentIds,
|
|
498
501
|
'progress-tracker',
|
|
499
502
|
undefined,
|
|
500
503
|
true,
|
|
@@ -504,6 +507,9 @@ export async function getRecentActivity({ page = 1, limit = 5, tabName = null }
|
|
|
504
507
|
addNextLesson: true,
|
|
505
508
|
}
|
|
506
509
|
)
|
|
510
|
+
contents = addAwardTemplateToContent(contents)
|
|
511
|
+
|
|
512
|
+
contents = await mapContentsThatWereLastProgressedFromMethod(contents)
|
|
507
513
|
|
|
508
514
|
recentActivityData.data = recentActivityData.data.map((practice) => {
|
|
509
515
|
const content = contents?.find((c) => c.id === practice.contentId) || {}
|
|
@@ -513,6 +519,7 @@ export async function getRecentActivity({ page = 1, limit = 5, tabName = null }
|
|
|
513
519
|
title: content.title,
|
|
514
520
|
parent_id: content.parent_id || null,
|
|
515
521
|
navigateTo: content.navigateTo,
|
|
522
|
+
sanityType: content.type || practice.sanityType,
|
|
516
523
|
artist_name: content.artist_name || null,
|
|
517
524
|
}
|
|
518
525
|
})
|
|
@@ -773,7 +780,7 @@ export async function calculateLongestStreaks(userId = globalConfig.sessionConfi
|
|
|
773
780
|
|
|
774
781
|
async function formatPracticeMeta(practices = []) {
|
|
775
782
|
const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
|
|
776
|
-
|
|
783
|
+
let contents = await addContextToContent(
|
|
777
784
|
fetchByRailContentIds,
|
|
778
785
|
contentIds,
|
|
779
786
|
'progress-tracker',
|
|
@@ -785,6 +792,9 @@ async function formatPracticeMeta(practices = []) {
|
|
|
785
792
|
addNextLesson: true,
|
|
786
793
|
}
|
|
787
794
|
)
|
|
795
|
+
contents = addAwardTemplateToContent(contents)
|
|
796
|
+
|
|
797
|
+
contents = await mapContentsThatWereLastProgressedFromMethod(contents)
|
|
788
798
|
|
|
789
799
|
return practices.map((practice) => {
|
|
790
800
|
const content =
|