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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(rg:*)",
5
+ "Bash(npm run lint:*)"
6
+ ],
7
+ "deny": []
8
+ }
9
+ }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "musora-content-services",
3
- "version": "2.123.2",
3
+ "version": "2.124.0",
4
4
  "description": "A package for Musoras content services ",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -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 { fetchByRailContentId, fetchByRailContentIds, fetchMethodV2Structure } from '../sanity.js'
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(response.children, LEARNING_PATH_LESSON, learningPathId)
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
- learningPath.children,
238
- LEARNING_PATH_LESSON,
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(lessons, parentContentType, parentContentId) {
272
+ export function mapContentToParent(
273
+ lessons: any,
274
+ options?: { lessonType?: string; parentContentId?: number }
275
+ ) {
265
276
  if (!lessons) return lessons
266
- return lessons.map((lesson: any) => {
267
- return { ...lesson, type: parentContentType, parent_id: parentContentId }
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
- const contents = recentContentIds.length > 0
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
- return playlistEngagedOnContents.length > 0
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
  }
@@ -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
- return fetchSanity(query, false, { customPostProcess: chapterProcess, processNeedAccess: true })
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 = ['learning-path-v2', 'learning-path-lesson-v2']
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 contents = await addContextToContent(
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
- contentIds,
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
- const contents = await addContextToContent(
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 =