musora-content-services 1.6.7 → 1.6.9

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.
Files changed (150) hide show
  1. package/.github/workflows/conventional-commits.yaml +0 -0
  2. package/.github/workflows/docs.js.yml +0 -0
  3. package/.github/workflows/sync-docs.yml +0 -0
  4. package/CHANGELOG.md +4 -0
  5. package/README.md +0 -0
  6. package/docs/algolia.js.html +0 -0
  7. package/docs/config.js.html +0 -0
  8. package/docs/index.html +0 -0
  9. package/docs/module-Config.html +0 -0
  10. package/docs/module-Railcontent-Services.html +0 -0
  11. package/docs/module-Sanity-Services.html +0 -0
  12. package/docs/module-Search-Engine-Services.html +0 -0
  13. package/docs/railcontent.js.html +0 -0
  14. package/docs/sanity.js.html +0 -0
  15. package/docs/v2/ContentOrganization.html +2 -2
  16. package/docs/v2/Forums.html +269 -0
  17. package/docs/v2/Gamification.html +2 -2
  18. package/docs/v2/TestUser.html +260 -0
  19. package/docs/v2/UserManagementSystem.html +2 -2
  20. package/docs/v2/api_types.js.html +2 -2
  21. package/docs/v2/config.js.html +2 -8
  22. package/docs/v2/content-org_content-org.js.html +2 -2
  23. package/docs/v2/content-org_guided-courses.ts.html +110 -0
  24. package/docs/v2/content-org_learning-paths.ts.html +123 -0
  25. package/docs/v2/content-org_playlists-types.js.html +14 -2
  26. package/docs/v2/content-org_playlists.js.html +39 -17
  27. package/docs/v2/content.js.html +111 -52
  28. package/docs/v2/fonts/Montserrat/Montserrat-Bold.eot +0 -0
  29. package/docs/v2/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
  30. package/docs/v2/fonts/Montserrat/Montserrat-Bold.woff +0 -0
  31. package/docs/v2/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
  32. package/docs/v2/fonts/Montserrat/Montserrat-Regular.eot +0 -0
  33. package/docs/v2/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
  34. package/docs/v2/fonts/Montserrat/Montserrat-Regular.woff +0 -0
  35. package/docs/v2/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
  36. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
  37. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
  38. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
  39. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
  40. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
  41. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
  42. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
  43. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
  44. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
  45. package/docs/v2/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
  46. package/docs/v2/forums_categories.ts.html +137 -0
  47. package/docs/v2/forums_discussions.js.html +95 -0
  48. package/docs/v2/forums_forum.js.html +95 -0
  49. package/docs/v2/forums_forums.ts.html +160 -0
  50. package/docs/v2/forums_posts.ts.html +258 -0
  51. package/docs/v2/forums_threads.ts.html +271 -0
  52. package/docs/v2/gamification_awards.js.html +35 -534
  53. package/docs/v2/gamification_awards.ts.html +181 -0
  54. package/docs/v2/gamification_gamification.js.html +2 -2
  55. package/docs/v2/gamification_types.js.html +7 -25
  56. package/docs/v2/global.html +849 -690
  57. package/docs/v2/index.html +2 -3
  58. package/docs/v2/liveTesting.ts.html +102 -0
  59. package/docs/v2/module-Accounts.html +2094 -0
  60. package/docs/v2/module-Awards.html +396 -14
  61. package/docs/v2/module-Categories.html +711 -0
  62. package/docs/v2/module-Config.html +4 -8
  63. package/docs/v2/module-Content-Services-V2.html +182 -49
  64. package/docs/v2/module-ForumCategories.html +687 -0
  65. package/docs/v2/module-ForumDiscussions.html +370 -0
  66. package/docs/v2/module-Forums.html +15393 -0
  67. package/docs/v2/module-GuidedCourses.html +1304 -0
  68. package/docs/v2/module-Interests.html +2 -2
  69. package/docs/v2/module-Onboarding.html +670 -0
  70. package/docs/v2/module-Payments.html +250 -0
  71. package/docs/v2/module-Permissions.html +2 -2
  72. package/docs/v2/module-Playlists.html +212 -44
  73. package/docs/v2/module-Railcontent-Services.html +689 -2175
  74. package/docs/v2/module-Sanity-Services.html +480 -925
  75. package/docs/v2/module-Sessions.html +3 -3
  76. package/docs/v2/module-Threads.html +1119 -0
  77. package/docs/v2/module-UserActivity.html +193 -23
  78. package/docs/v2/module-UserChat.html +410 -0
  79. package/docs/v2/module-UserManagement.html +1028 -11
  80. package/docs/v2/module-UserMemberships.html +829 -0
  81. package/docs/v2/module-UserNotifications.html +1399 -27
  82. package/docs/v2/module-UserProfile.html +106 -2
  83. package/docs/v2/railcontent.js.html +87 -236
  84. package/docs/v2/sanity.js.html +342 -415
  85. package/docs/v2/scripts/collapse.js +0 -0
  86. package/docs/v2/scripts/commonNav.js +0 -0
  87. package/docs/v2/scripts/linenumber.js +0 -0
  88. package/docs/v2/scripts/nav.js +0 -0
  89. package/docs/v2/scripts/polyfill.js +0 -0
  90. package/docs/v2/scripts/prettify/Apache-License-2.0.txt +0 -0
  91. package/docs/v2/scripts/prettify/lang-css.js +0 -0
  92. package/docs/v2/scripts/prettify/prettify.js +0 -0
  93. package/docs/v2/scripts/search.js +0 -0
  94. package/docs/v2/styles/jsdoc.css +0 -0
  95. package/docs/v2/styles/prettify.css +0 -0
  96. package/docs/v2/userActivity.js.html +532 -451
  97. package/docs/v2/user_account.ts.html +237 -0
  98. package/docs/v2/user_chat.js.html +98 -0
  99. package/docs/v2/user_interests.js.html +2 -2
  100. package/docs/v2/user_management.js.html +82 -2
  101. package/docs/v2/user_memberships.js.html +144 -0
  102. package/docs/v2/user_memberships.ts.html +292 -0
  103. package/docs/v2/user_notifications.js.html +203 -21
  104. package/docs/v2/user_onboarding.ts.html +183 -0
  105. package/docs/v2/user_payments.ts.html +97 -0
  106. package/docs/v2/user_permissions.js.html +2 -2
  107. package/docs/v2/user_profile.js.html +12 -2
  108. package/docs/v2/user_sessions.js.html +33 -2
  109. package/docs/v2/user_types.js.html +16 -2
  110. package/docs/v2/user_user-management-system.js.html +2 -2
  111. package/jsdoc.json +0 -0
  112. package/link_mcs.sh +0 -0
  113. package/package.json +1 -1
  114. package/src/contentMetaData.js +0 -0
  115. package/src/contentTypeConfig.js +3 -3
  116. package/src/filterBuilder.js +0 -0
  117. package/src/index.d.ts +0 -0
  118. package/src/index.js +0 -0
  119. package/src/services/algolia.js +0 -0
  120. package/src/services/config.js +0 -0
  121. package/src/services/contentLikes.js +0 -0
  122. package/src/services/contentProgress.js +0 -0
  123. package/src/services/dataContext.js +0 -0
  124. package/src/services/imageSRCBuilder.js +1 -1
  125. package/src/services/imageSRCVerify.js +1 -1
  126. package/src/services/lastUpdated.js +0 -0
  127. package/src/services/railcontent.js +0 -0
  128. package/src/services/recommendations.js +0 -0
  129. package/src/services/userPermissions.js +0 -0
  130. package/test/config.test.js +0 -0
  131. package/test/contentLikes.test.js +0 -0
  132. package/test/contentProgress.test.js +0 -0
  133. package/test/imageSRCBuilder.test.js +1 -1
  134. package/test/imageSRCVerify.test.js +4 -4
  135. package/test/initializeTests.js +0 -0
  136. package/test/lastUpdated.test.js +0 -0
  137. package/test/sanityQueryService.test.js +0 -0
  138. package/test/userPermissions.test.js +0 -0
  139. package/tools/generate-index.cjs +0 -0
  140. package/.yarnrc.yml +0 -1
  141. package/docs/v2/Content-Organization.html +0 -245
  142. package/docs/v2/UserManagement.html +0 -269
  143. package/docs/v2/global.html#User +0 -293
  144. package/docs/v2/module-Notifications.html +0 -1183
  145. package/docs/v2/module-Session-Management.html +0 -575
  146. package/docs/v2/module-User-Activity.html +0 -4410
  147. package/docs/v2/module-User-Management.html +0 -490
  148. package/docs/v2/module-User-Permissions.html +0 -406
  149. package/docs/v2/types.js.html +0 -122
  150. package/docs/v2/user_user-management.js.html +0 -78
@@ -29,7 +29,7 @@
29
29
  <nav >
30
30
 
31
31
 
32
- <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-Awards.html">Awards</a><ul class='methods'><li data-type='method'><a href="module-Awards.html#.fetchAwardsForUser">fetchAwardsForUser</a></li></ul></li><li><a href="module-Config.html">Config</a><ul class='methods'><li data-type='method'><a href="module-Config.html#.initializeService">initializeService</a></li></ul></li><li><a href="module-Content-Services-V2.html">Content-Services-V2</a><ul class='methods'><li data-type='method'><a href="module-Content-Services-V2.html#.getContentRows">getContentRows</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getNewAndUpcoming">getNewAndUpcoming</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getRecent">getRecent</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getRecommendedForYou">getRecommendedForYou</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getScheduleContentRows">getScheduleContentRows</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getTabResults">getTabResults</a></li></ul></li><li><a href="module-Interests.html">Interests</a><ul class='methods'><li data-type='method'><a href="module-Interests.html#.fetchInterests">fetchInterests</a></li><li data-type='method'><a href="module-Interests.html#.fetchUninterests">fetchUninterests</a></li><li data-type='method'><a href="module-Interests.html#.markContentAsInterested">markContentAsInterested</a></li><li data-type='method'><a href="module-Interests.html#.markContentAsNotInterested">markContentAsNotInterested</a></li><li data-type='method'><a href="module-Interests.html#.removeContentAsInterested">removeContentAsInterested</a></li><li data-type='method'><a href="module-Interests.html#.removeContentAsNotInterested">removeContentAsNotInterested</a></li></ul></li><li><a href="module-Permissions.html">Permissions</a><ul class='methods'><li data-type='method'><a href="module-Permissions.html#.fetchUserPermissions">fetchUserPermissions</a></li><li data-type='method'><a href="module-Permissions.html#.reset">reset</a></li></ul></li><li><a href="module-Playlists.html">Playlists</a><ul class='methods'><li data-type='method'><a href="module-Playlists.html#.addItemToPlaylist">addItemToPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.createPlaylist">createPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.deletePlaylist">deletePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.duplicatePlaylist">duplicatePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.fetchPlaylist">fetchPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.fetchPlaylistItems">fetchPlaylistItems</a></li><li data-type='method'><a href="module-Playlists.html#.fetchUserPlaylists">fetchUserPlaylists</a></li><li data-type='method'><a href="module-Playlists.html#.togglePlaylistPrivate">togglePlaylistPrivate</a></li><li data-type='method'><a href="module-Playlists.html#.undeletePlaylist">undeletePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.updatePlaylist">updatePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~likePlaylist">likePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~reportPlaylist">reportPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~togglePlaylistPrivate">togglePlaylistPrivate</a></li><li data-type='method'><a href="module-Playlists.html#~unlikePlaylist">unlikePlaylist</a></li></ul></li><li><a href="module-Railcontent-Services.html">Railcontent-Services</a><ul class='methods'><li data-type='method'><a href="module-Railcontent-Services.html#.assignModeratorToComment">assignModeratorToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.closeComment">closeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.createComment">createComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.deleteComment">deleteComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.editComment">editComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchAllCompletedStates">fetchAllCompletedStates</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCarouselCardData">fetchCarouselCardData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchChallengeIndexMetadata">fetchChallengeIndexMetadata</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchChallengeLessonData">fetchChallengeLessonData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchChallengeMetadata">fetchChallengeMetadata</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchChallengeUserActiveChallenges">fetchChallengeUserActiveChallenges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCommentRelies">fetchCommentRelies</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchComments">fetchComments</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedChallenges">fetchCompletedChallenges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedContent">fetchCompletedContent</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedState">fetchCompletedState</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchContentInProgress">fetchContentInProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchContentPageUserData">fetchContentPageUserData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchNextContentDataForParent">fetchNextContentDataForParent</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchOwnedChallenges">fetchOwnedChallenges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchRecentUserActivities">fetchRecentUserActivities</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchSongsInProgress">fetchSongsInProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchTopComment">fetchTopComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserAward">fetchUserAward</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserBadges">fetchUserBadges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserChallengeProgress">fetchUserChallengeProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserPracticeNotes">fetchUserPracticeNotes</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.likeComment">likeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.openComment">openComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesCommunityNotification">postChallengesCommunityNotification</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesEnroll">postChallengesEnroll</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesEnrollmentNotification">postChallengesEnrollmentNotification</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesHideCompletedBanner">postChallengesHideCompletedBanner</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesLeave">postChallengesLeave</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesSetStartDate">postChallengesSetStartDate</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesSoloNotification">postChallengesSoloNotification</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postChallengesUnlock">postChallengesUnlock</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.replyToComment">replyToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.reportComment">reportComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.setStudentViewForUser">setStudentViewForUser</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.unassignModeratorToComment">unassignModeratorToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.unlikeComment">unlikeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#~fetchLastInteractedChild">fetchLastInteractedChild</a></li></ul></li><li><a href="module-Sanity-Services.html">Sanity-Services</a><ul class='methods'><li data-type='method'><a href="module-Sanity-Services.html#.fetchAll">fetchAll</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchAllFilterOptions">fetchAllFilterOptions</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchAllPacks">fetchAllPacks</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchArtistLessons">fetchArtistLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchArtists">fetchArtists</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByRailContentId">fetchByRailContentId</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByRailContentIds">fetchByRailContentIds</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByReference">fetchByReference</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchCoachLessons">fetchCoachLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchComingSoon">fetchComingSoon</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchCommentModContentData">fetchCommentModContentData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchFoundation">fetchFoundation</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchGenreLessons">fetchGenreLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLeaving">fetchLeaving</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLessonContent">fetchLessonContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLessonsFeaturingThisContent">fetchLessonsFeaturingThisContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMetadata">fetchMetadata</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethod">fetchMethod</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodChildren">fetchMethodChildren</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodChildrenIds">fetchMethodChildrenIds</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodPreviousNextLesson">fetchMethodPreviousNextLesson</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchNewReleases">fetchNewReleases</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchNextPreviousLesson">fetchNextPreviousLesson</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchOtherSongVersions">fetchOtherSongVersions</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchPackAll">fetchPackAll</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchPackData">fetchPackData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchParentForDownload">fetchParentForDownload</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedLessons">fetchRelatedLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedRecommendedContent">fetchRelatedRecommendedContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedSongs">fetchRelatedSongs</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchReturning">fetchReturning</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSanity">fetchSanity</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchScheduledReleases">fetchScheduledReleases</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchShowsData">fetchShowsData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSongArtistCount">fetchSongArtistCount</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSongById">fetchSongById</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchUpcomingEvents">fetchUpcomingEvents</a></li><li data-type='method'><a href="module-Sanity-Services.html#.jumpToContinueContent">jumpToContinueContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#~fetchRelatedByLicense">fetchRelatedByLicense</a></li><li data-type='method'><a href="module-Sanity-Services.html#~getNextAndPreviousQuarterDates">getNextAndPreviousQuarterDates</a></li><li data-type='method'><a href="module-Sanity-Services.html#~getQueryFromPage">getQueryFromPage</a></li><li data-type='method'><a href="module-Sanity-Services.html#~handleCustomFetchAll">handleCustomFetchAll</a></li></ul></li><li><a href="module-Sessions.html">Sessions</a><ul class='methods'><li data-type='method'><a href="module-Sessions.html#.login">login</a></li><li data-type='method'><a href="module-Sessions.html#.logout">logout</a></li></ul></li><li><a href="module-UserActivity.html">UserActivity</a><ul class='methods'><li data-type='method'><a href="module-UserActivity.html#.calculateLongestStreaks">calculateLongestStreaks</a></li><li data-type='method'><a href="module-UserActivity.html#.createPracticeNotes">createPracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.deletePracticeSession">deletePracticeSession</a></li><li data-type='method'><a href="module-UserActivity.html#.deleteUserActivity">deleteUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.getPracticeNotes">getPracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.getPracticeSessions">getPracticeSessions</a></li><li data-type='method'><a href="module-UserActivity.html#.getProgressRows">getProgressRows</a></li><li data-type='method'><a href="module-UserActivity.html#.getRecentActivity">getRecentActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.getUserMonthlyStats">getUserMonthlyStats</a></li><li data-type='method'><a href="module-UserActivity.html#.getUserWeeklyStats">getUserWeeklyStats</a></li><li data-type='method'><a href="module-UserActivity.html#.pinProgressRow">pinProgressRow</a></li><li data-type='method'><a href="module-UserActivity.html#.recordUserActivity">recordUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.recordUserPractice">recordUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.removeUserPractice">removeUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.restorePracticeSession">restorePracticeSession</a></li><li data-type='method'><a href="module-UserActivity.html#.restoreUserPractice">restoreUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.unpinProgressRow">unpinProgressRow</a></li><li data-type='method'><a href="module-UserActivity.html#.updatePracticeNotes">updatePracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.updateUserPractice">updateUserPractice</a></li></ul></li><li><a href="module-UserChat.html">UserChat</a><ul class='methods'><li data-type='method'><a href="module-UserChat.html#.fetchChatSettings">fetchChatSettings</a></li></ul></li><li><a href="module-UserManagement.html">UserManagement</a><ul class='methods'><li data-type='method'><a href="module-UserManagement.html#.blockUser">blockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.deletePicture">deletePicture</a></li><li data-type='method'><a href="module-UserManagement.html#.unblockUser">unblockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.uploadPicture">uploadPicture</a></li><li data-type='method'><a href="module-UserManagement.html#.uploadPictureFromS3">uploadPictureFromS3</a></li></ul></li><li><a href="module-UserNotifications.html">UserNotifications</a><ul class='methods'><li data-type='method'><a href="module-UserNotifications.html#.deleteNotification">deleteNotification</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchNotifications">fetchNotifications</a></li><li data-type='method'><a href="module-UserNotifications.html#.markAllNotificationsAsRead">markAllNotificationsAsRead</a></li><li data-type='method'><a href="module-UserNotifications.html#.markNotificationAsRead">markNotificationAsRead</a></li><li data-type='method'><a href="module-UserNotifications.html#.markNotificationAsUnread">markNotificationAsUnread</a></li></ul></li><li><a href="module-UserProfile.html">UserProfile</a><ul class='methods'><li data-type='method'><a href="module-UserProfile.html#.otherStats">otherStats</a></li></ul></li></ul><h3>Namespaces</h3><ul><li><a href="ContentOrganization.html">ContentOrganization</a></li><li><a href="Gamification.html">Gamification</a></li><li><a href="UserManagementSystem.html">UserManagementSystem</a></li></ul><h3><a href="global.html">Global</a></h3>
32
+ <h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-Accounts.html">Accounts</a><ul class='methods'><li data-type='method'><a href="module-Accounts.html#~confirmEmailChange">confirmEmailChange</a></li><li data-type='method'><a href="module-Accounts.html#~deleteAccount">deleteAccount</a></li><li data-type='method'><a href="module-Accounts.html#~numberOfActiveUsers">numberOfActiveUsers</a></li><li data-type='method'><a href="module-Accounts.html#~requestEmailChange">requestEmailChange</a></li><li data-type='method'><a href="module-Accounts.html#~resetPassword">resetPassword</a></li><li data-type='method'><a href="module-Accounts.html#~sendAccountSetupEmail">sendAccountSetupEmail</a></li><li data-type='method'><a href="module-Accounts.html#~sendPasswordResetEmail">sendPasswordResetEmail</a></li><li data-type='method'><a href="module-Accounts.html#~setupAccount">setupAccount</a></li><li data-type='method'><a href="module-Accounts.html#~status">status</a></li></ul></li><li><a href="module-Awards.html">Awards</a><ul class='methods'><li data-type='method'><a href="module-Awards.html#~fetchAwardsForUser">fetchAwardsForUser</a></li><li data-type='method'><a href="module-Awards.html#~fetchCertificate">fetchCertificate</a></li><li data-type='method'><a href="module-Awards.html#~getAwardDataForGuidedContent">getAwardDataForGuidedContent</a></li></ul></li><li><a href="module-Config.html">Config</a><ul class='methods'><li data-type='method'><a href="module-Config.html#.initializeService">initializeService</a></li></ul></li><li><a href="module-Content-Services-V2.html">Content-Services-V2</a><ul class='methods'><li data-type='method'><a href="module-Content-Services-V2.html#.getContentRows">getContentRows</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getLegacyMethods">getLegacyMethods</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getNewAndUpcoming">getNewAndUpcoming</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getRecent">getRecent</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getRecommendedForYou">getRecommendedForYou</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getScheduleContentRows">getScheduleContentRows</a></li><li data-type='method'><a href="module-Content-Services-V2.html#.getTabResults">getTabResults</a></li></ul></li><li><a href="module-Forums.html">Forums</a><ul class='methods'><li data-type='method'><a href="module-Forums.html#~createForumCategory">createForumCategory</a></li><li data-type='method'><a href="module-Forums.html#~createPost">createPost</a></li><li data-type='method'><a href="module-Forums.html#~createThread">createThread</a></li><li data-type='method'><a href="module-Forums.html#~deletePost">deletePost</a></li><li data-type='method'><a href="module-Forums.html#~deleteThread">deleteThread</a></li><li data-type='method'><a href="module-Forums.html#~fetchCommunityGuidelines">fetchCommunityGuidelines</a></li><li data-type='method'><a href="module-Forums.html#~fetchFollowedThreads">fetchFollowedThreads</a></li><li data-type='method'><a href="module-Forums.html#~fetchForumCategories">fetchForumCategories</a></li><li data-type='method'><a href="module-Forums.html#~fetchLatestThreads">fetchLatestThreads</a></li><li data-type='method'><a href="module-Forums.html#~fetchPosts">fetchPosts</a></li><li data-type='method'><a href="module-Forums.html#~fetchThreads">fetchThreads</a></li><li data-type='method'><a href="module-Forums.html#~followThread">followThread</a></li><li data-type='method'><a href="module-Forums.html#~jumpToPost">jumpToPost</a></li><li data-type='method'><a href="module-Forums.html#~likePost">likePost</a></li><li data-type='method'><a href="module-Forums.html#~lockThread">lockThread</a></li><li data-type='method'><a href="module-Forums.html#~pinThread">pinThread</a></li><li data-type='method'><a href="module-Forums.html#~search">search</a></li><li data-type='method'><a href="module-Forums.html#~unfollowThread">unfollowThread</a></li><li data-type='method'><a href="module-Forums.html#~unlikePost">unlikePost</a></li><li data-type='method'><a href="module-Forums.html#~unlockThread">unlockThread</a></li><li data-type='method'><a href="module-Forums.html#~unpinThread">unpinThread</a></li><li data-type='method'><a href="module-Forums.html#~updateForumCategory">updateForumCategory</a></li><li data-type='method'><a href="module-Forums.html#~updatePost">updatePost</a></li><li data-type='method'><a href="module-Forums.html#~updateThread">updateThread</a></li></ul></li><li></li><li></li><li><a href="module-GuidedCourses.html">GuidedCourses</a><ul class='methods'><li data-type='method'><a href="module-GuidedCourses.html#~getActivePath">getActivePath</a></li><li data-type='method'><a href="module-GuidedCourses.html#~getDailySession">getDailySession</a></li><li data-type='method'><a href="module-GuidedCourses.html#~updateActivePath">updateActivePath</a></li><li data-type='method'><a href="module-GuidedCourses.html#~updateDailySession">updateDailySession</a></li></ul></li><li></li><li><a href="module-Interests.html">Interests</a><ul class='methods'><li data-type='method'><a href="module-Interests.html#.fetchInterests">fetchInterests</a></li><li data-type='method'><a href="module-Interests.html#.fetchUninterests">fetchUninterests</a></li><li data-type='method'><a href="module-Interests.html#.markContentAsInterested">markContentAsInterested</a></li><li data-type='method'><a href="module-Interests.html#.markContentAsNotInterested">markContentAsNotInterested</a></li><li data-type='method'><a href="module-Interests.html#.removeContentAsInterested">removeContentAsInterested</a></li><li data-type='method'><a href="module-Interests.html#.removeContentAsNotInterested">removeContentAsNotInterested</a></li></ul></li><li><a href="module-Onboarding.html">Onboarding</a><ul class='methods'><li data-type='method'><a href="module-Onboarding.html#~startOnboarding">startOnboarding</a></li><li data-type='method'><a href="module-Onboarding.html#~updateOnboarding">updateOnboarding</a></li><li data-type='method'><a href="module-Onboarding.html#~userOnboardingForBrand">userOnboardingForBrand</a></li></ul></li><li><a href="module-Payments.html">Payments</a><ul class='methods'><li data-type='method'><a href="module-Payments.html#~fetchCustomerPayments">fetchCustomerPayments</a></li></ul></li><li><a href="module-Permissions.html">Permissions</a><ul class='methods'><li data-type='method'><a href="module-Permissions.html#.fetchUserPermissions">fetchUserPermissions</a></li><li data-type='method'><a href="module-Permissions.html#.reset">reset</a></li></ul></li><li><a href="module-Playlists.html">Playlists</a><ul class='methods'><li data-type='method'><a href="module-Playlists.html#.addItemToPlaylist">addItemToPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.createPlaylist">createPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.deletePlaylist">deletePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.duplicatePlaylist">duplicatePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.fetchPlaylist">fetchPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.fetchPlaylistItems">fetchPlaylistItems</a></li><li data-type='method'><a href="module-Playlists.html#.fetchUserPlaylists">fetchUserPlaylists</a></li><li data-type='method'><a href="module-Playlists.html#.togglePlaylistPrivate">togglePlaylistPrivate</a></li><li data-type='method'><a href="module-Playlists.html#.undeletePlaylist">undeletePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#.updatePlaylist">updatePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~deleteItemsFromPlaylist">deleteItemsFromPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~likePlaylist">likePlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~reportPlaylist">reportPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~restoreItemFromPlaylist">restoreItemFromPlaylist</a></li><li data-type='method'><a href="module-Playlists.html#~unlikePlaylist">unlikePlaylist</a></li></ul></li><li><a href="module-Railcontent-Services.html">Railcontent-Services</a><ul class='methods'><li data-type='method'><a href="module-Railcontent-Services.html#.assignModeratorToComment">assignModeratorToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.closeComment">closeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.createComment">createComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.deleteComment">deleteComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.editComment">editComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchAllCompletedStates">fetchAllCompletedStates</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCarouselCardData">fetchCarouselCardData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchComment">fetchComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCommentRelies">fetchCommentRelies</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchComments">fetchComments</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedContent">fetchCompletedContent</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchCompletedState">fetchCompletedState</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchContentInProgress">fetchContentInProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchContentPageUserData">fetchContentPageUserData</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchNextContentDataForParent">fetchNextContentDataForParent</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchRecentUserActivities">fetchRecentUserActivities</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchSongsInProgress">fetchSongsInProgress</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchTopComment">fetchTopComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserAward">fetchUserAward</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserBadges">fetchUserBadges</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.fetchUserPracticeNotes">fetchUserPracticeNotes</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.likeComment">likeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.openComment">openComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postContentComplete">postContentComplete</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postContentReset">postContentReset</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postContentRestore">postContentRestore</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.postContentStart">postContentStart</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.replyToComment">replyToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.reportComment">reportComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.restoreComment">restoreComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.setStudentViewForUser">setStudentViewForUser</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.unassignModeratorToComment">unassignModeratorToComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#.unlikeComment">unlikeComment</a></li><li data-type='method'><a href="module-Railcontent-Services.html#~fetchLastInteractedChild">fetchLastInteractedChild</a></li></ul></li><li><a href="module-Sanity-Services.html">Sanity-Services</a><ul class='methods'><li data-type='method'><a href="module-Sanity-Services.html#.fetchAll">fetchAll</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchAllFilterOptions">fetchAllFilterOptions</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchAllPacks">fetchAllPacks</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchArtistLessons">fetchArtistLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchArtists">fetchArtists</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByRailContentId">fetchByRailContentId</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByRailContentIds">fetchByRailContentIds</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchByReference">fetchByReference</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchCoachLessons">fetchCoachLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchComingSoon">fetchComingSoon</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchCommentModContentData">fetchCommentModContentData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchFoundation">fetchFoundation</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchGenreLessons">fetchGenreLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLeaving">fetchLeaving</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLessonContent">fetchLessonContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchLessonsFeaturingThisContent">fetchLessonsFeaturingThisContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMetadata">fetchMetadata</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethod">fetchMethod</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodChildren">fetchMethodChildren</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodChildrenIds">fetchMethodChildrenIds</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodPreviousNextLesson">fetchMethodPreviousNextLesson</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodV2IntroVideo">fetchMethodV2IntroVideo</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchMethodV2Structure">fetchMethodV2Structure</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchNewReleases">fetchNewReleases</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchNextPreviousLesson">fetchNextPreviousLesson</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchOtherSongVersions">fetchOtherSongVersions</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchPackAll">fetchPackAll</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchPackData">fetchPackData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedLessons">fetchRelatedLessons</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedRecommendedContent">fetchRelatedRecommendedContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchRelatedSongs">fetchRelatedSongs</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchReturning">fetchReturning</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSanity">fetchSanity</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchScheduledReleases">fetchScheduledReleases</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchShowsData">fetchShowsData</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSiblingContent">fetchSiblingContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSongArtistCount">fetchSongArtistCount</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchSongById">fetchSongById</a></li><li data-type='method'><a href="module-Sanity-Services.html#.fetchUpcomingEvents">fetchUpcomingEvents</a></li><li data-type='method'><a href="module-Sanity-Services.html#.jumpToContinueContent">jumpToContinueContent</a></li><li data-type='method'><a href="module-Sanity-Services.html#~fetchRelatedByLicense">fetchRelatedByLicense</a></li><li data-type='method'><a href="module-Sanity-Services.html#~getQueryFromPage">getQueryFromPage</a></li></ul></li><li><a href="module-Sessions.html">Sessions</a><ul class='methods'><li data-type='method'><a href="module-Sessions.html#.login">login</a></li><li data-type='method'><a href="module-Sessions.html#.logout">logout</a></li></ul></li><li><a href="module-UserActivity.html">UserActivity</a><ul class='methods'><li data-type='method'><a href="module-UserActivity.html#.calculateLongestStreaks">calculateLongestStreaks</a></li><li data-type='method'><a href="module-UserActivity.html#.createPracticeNotes">createPracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.deletePracticeSession">deletePracticeSession</a></li><li data-type='method'><a href="module-UserActivity.html#.deleteUserActivity">deleteUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.getPracticeNotes">getPracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.getPracticeSessions">getPracticeSessions</a></li><li data-type='method'><a href="module-UserActivity.html#.getProgressRows">getProgressRows</a></li><li data-type='method'><a href="module-UserActivity.html#.getRecentActivity">getRecentActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.getUserMonthlyStats">getUserMonthlyStats</a></li><li data-type='method'><a href="module-UserActivity.html#.getUserWeeklyStats">getUserWeeklyStats</a></li><li data-type='method'><a href="module-UserActivity.html#.pinProgressRow">pinProgressRow</a></li><li data-type='method'><a href="module-UserActivity.html#.recordUserActivity">recordUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.recordUserPractice">recordUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.removeUserPractice">removeUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.restorePracticeSession">restorePracticeSession</a></li><li data-type='method'><a href="module-UserActivity.html#.restoreUserActivity">restoreUserActivity</a></li><li data-type='method'><a href="module-UserActivity.html#.restoreUserPractice">restoreUserPractice</a></li><li data-type='method'><a href="module-UserActivity.html#.unpinProgressRow">unpinProgressRow</a></li><li data-type='method'><a href="module-UserActivity.html#.updatePracticeNotes">updatePracticeNotes</a></li><li data-type='method'><a href="module-UserActivity.html#.updateUserPractice">updateUserPractice</a></li></ul></li><li><a href="module-UserChat.html">UserChat</a><ul class='methods'><li data-type='method'><a href="module-UserChat.html#.fetchChatSettings">fetchChatSettings</a></li></ul></li><li><a href="module-UserManagement.html">UserManagement</a><ul class='methods'><li data-type='method'><a href="module-UserManagement.html#.blockUser">blockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.blockedUsers">blockedUsers</a></li><li data-type='method'><a href="module-UserManagement.html#.deletePicture">deletePicture</a></li><li data-type='method'><a href="module-UserManagement.html#.getUserData">getUserData</a></li><li data-type='method'><a href="module-UserManagement.html#.getUserSignature">getUserSignature</a></li><li data-type='method'><a href="module-UserManagement.html#.isUsernameAvailable">isUsernameAvailable</a></li><li data-type='method'><a href="module-UserManagement.html#.setUserSignature">setUserSignature</a></li><li data-type='method'><a href="module-UserManagement.html#.toggleSignaturePrivate">toggleSignaturePrivate</a></li><li data-type='method'><a href="module-UserManagement.html#.unblockUser">unblockUser</a></li><li data-type='method'><a href="module-UserManagement.html#.updateDisplayName">updateDisplayName</a></li><li data-type='method'><a href="module-UserManagement.html#.uploadPicture">uploadPicture</a></li><li data-type='method'><a href="module-UserManagement.html#.uploadPictureFromS3">uploadPictureFromS3</a></li></ul></li><li><a href="module-UserMemberships.html">UserMemberships</a><ul class='methods'><li data-type='method'><a href="module-UserMemberships.html#~fetchMemberships">fetchMemberships</a></li><li data-type='method'><a href="module-UserMemberships.html#~fetchRechargeTokens">fetchRechargeTokens</a></li><li data-type='method'><a href="module-UserMemberships.html#~restorePurchases">restorePurchases</a></li><li data-type='method'><a href="module-UserMemberships.html#~upgradeSubscription">upgradeSubscription</a></li></ul></li><li><a href="module-UserNotifications.html">UserNotifications</a><ul class='methods'><li data-type='method'><a href="module-UserNotifications.html#.deleteNotification">deleteNotification</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchLiveEventPollingState">fetchLiveEventPollingState</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchNotificationSettings">fetchNotificationSettings</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchNotifications">fetchNotifications</a></li><li data-type='method'><a href="module-UserNotifications.html#.fetchUnreadCount">fetchUnreadCount</a></li><li data-type='method'><a href="module-UserNotifications.html#.markAllNotificationsAsRead">markAllNotificationsAsRead</a></li><li data-type='method'><a href="module-UserNotifications.html#.markNotificationAsRead">markNotificationAsRead</a></li><li data-type='method'><a href="module-UserNotifications.html#.markNotificationAsUnread">markNotificationAsUnread</a></li><li data-type='method'><a href="module-UserNotifications.html#.pauseLiveEventPolling">pauseLiveEventPolling</a></li><li data-type='method'><a href="module-UserNotifications.html#.restoreNotification">restoreNotification</a></li><li data-type='method'><a href="module-UserNotifications.html#.startLiveEventPolling">startLiveEventPolling</a></li><li data-type='method'><a href="module-UserNotifications.html#.updateNotificationSetting">updateNotificationSetting</a></li></ul></li><li><a href="module-UserProfile.html">UserProfile</a><ul class='methods'><li data-type='method'><a href="module-UserProfile.html#.deleteProfilePicture">deleteProfilePicture</a></li><li data-type='method'><a href="module-UserProfile.html#.otherStats">otherStats</a></li></ul></li></ul><h3>Namespaces</h3><ul><li><a href="ContentOrganization.html">ContentOrganization</a></li><li><a href="Forums.html">Forums</a></li><li><a href="Gamification.html">Gamification</a></li><li><a href="UserManagementSystem.html">UserManagementSystem</a></li></ul><h3>Interfaces</h3><ul><li><a href="TestUser.html">TestUser</a></li></ul><h3>Global</h3><ul><li><a href="global.html#createTestUser">createTestUser</a></li></ul>
33
33
 
34
34
  </nav>
35
35
 
@@ -55,16 +55,34 @@ import {
55
55
  fetchUserPracticeMeta,
56
56
  fetchUserPracticeNotes,
57
57
  fetchHandler,
58
- fetchRecentUserActivities, fetchChallengeLessonData
58
+ fetchRecentUserActivities,
59
59
  } from './railcontent'
60
60
  import { DataContext, UserActivityVersionKey } from './dataContext.js'
61
- import { fetchByRailContentIds, fetchShows } from './sanity'
62
- import {fetchPlaylist, fetchUserPlaylists} from "./content-org/playlists";
63
- import {convertToTimeZone, getMonday, getWeekNumber, isSameDate, isNextDay, getTimeRemainingUntilLocal} from './dateUtils.js'
61
+ import { fetchByRailContentId, fetchByRailContentIds, fetchShows } from './sanity'
62
+ import { fetchPlaylist, fetchUserPlaylists } from './content-org/playlists'
63
+ import {guidedCourses} from './content-org/guided-courses'
64
+ import {
65
+ getMonday,
66
+ getWeekNumber,
67
+ isSameDate,
68
+ isNextDay,
69
+ getTimeRemainingUntilLocal,
70
+ toDayjs,
71
+ } from './dateUtils.js'
64
72
  import { globalConfig } from './config'
65
- import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping, showsLessonTypes, songs} from "../contentTypeConfig";
66
- import {getAllStartedOrCompleted, getProgressStateByIds} from "./contentProgress";
67
- import {TabResponseType} from "../contentMetaData";
73
+ import {
74
+ collectionLessonTypes,
75
+ progressTypesMapping,
76
+ recentTypes,
77
+ showsLessonTypes,
78
+ songs,
79
+ } from '../contentTypeConfig'
80
+ import { getAllStartedOrCompleted, getProgressStateByIds } from './contentProgress'
81
+ import { TabResponseType } from '../contentMetaData'
82
+ import dayjs from 'dayjs'
83
+ import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
84
+ import weekOfYear from 'dayjs/plugin/weekOfYear'
85
+ import { addContextToContent } from './contentAggregator.js'
68
86
 
69
87
  const DATA_KEY_PRACTICES = 'practices'
70
88
  const DATA_KEY_LAST_UPDATED_TIME = 'u'
@@ -102,7 +120,7 @@ function getIndefiniteArticle(streak) {
102
120
 
103
121
  export async function getUserPractices(userId = globalConfig.sessionConfig.userId) {
104
122
  if (userId !== globalConfig.sessionConfig.userId) {
105
- let data = await fetchUserPractices({ userId })
123
+ let data = await fetchUserPractices(0, { userId: userId })
106
124
  return data?.['data']?.[DATA_KEY_PRACTICES] ?? {}
107
125
  } else {
108
126
  let data = await userActivityContext.getData()
@@ -124,24 +142,30 @@ export let userActivityContext = new DataContext(UserActivityVersionKey, fetchUs
124
142
  * .catch(error => console.error(error));
125
143
  */
126
144
  export async function getUserWeeklyStats() {
145
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
127
146
  let data = await userActivityContext.getData()
128
147
  let practices = data?.[DATA_KEY_PRACTICES] ?? {}
129
148
  let sortedPracticeDays = Object.keys(practices)
130
- .map((date) => new Date(date))
131
- .sort((a, b) => b - a)
132
-
133
- let today = new Date()
134
- today.setHours(0, 0, 0, 0)
135
- let startOfWeek = getMonday(today) // Get last Monday
149
+ .map((date) => toDayjs(date)) // Convert to dayjs instance
150
+ .sort((a, b) => b.valueOf() - a.valueOf())
151
+ let today = dayjs()
152
+ let startOfWeek = getMonday(today, timeZone) // Get last Monday
136
153
  let dailyStats = []
137
-
138
154
  for (let i = 0; i &lt; 7; i++) {
139
- let day = new Date(startOfWeek)
140
- day.setDate(startOfWeek.getDate() + i)
141
- let hasPractice = sortedPracticeDays.some((practiceDate) => isSameDate(practiceDate, day))
142
- let isActive = isSameDate(today, day)
155
+ const day = startOfWeek.add(i, 'day')
156
+ let hasPractice = sortedPracticeDays.some((practiceDate) =>
157
+ isSameDate(practiceDate, day.format('YYYY-MM-DD'))
158
+ )
159
+ let isActive = isSameDate(today.format(), day.format())
143
160
  let type = hasPractice ? 'tracked' : isActive ? 'active' : 'none'
144
- dailyStats.push({ key: i, label: DAYS[i], isActive, inStreak: hasPractice, type })
161
+ dailyStats.push({
162
+ key: i,
163
+ label: DAYS[i],
164
+ isActive,
165
+ inStreak: hasPractice,
166
+ type,
167
+ day: day.format('YYYY-MM-DD'),
168
+ })
145
169
  }
146
170
 
147
171
  let { streakMessage } = getStreaksAndMessage(practices)
@@ -180,83 +204,77 @@ export async function getUserWeeklyStats() {
180
204
  * getUserMonthlyStats({ userId: 123 }).then(console.log);
181
205
  */
182
206
  export async function getUserMonthlyStats(params = {}) {
183
- const now = new Date()
207
+ const now = dayjs()
184
208
  const {
185
- year = now.getFullYear(),
186
- month = now.getMonth(),
187
- day = 1,
209
+ year = now.year(),
210
+ month = now.month(), // 0-indexed
188
211
  userId = globalConfig.sessionConfig.userId,
189
212
  } = params
190
- let practices = await getUserPractices(userId)
213
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
214
+ const practices = await getUserPractices(userId)
191
215
 
192
- // Get the first day of the specified month and the number of days in that month
193
- let firstDayOfMonth = new Date(year, month, 1)
194
- let today = new Date()
195
- today.setHours(0, 0, 0, 0)
216
+ const firstDayOfMonth = dayjs.tz(`${year}-${month + 1}-01`, timeZone).startOf('day')
217
+ const endOfMonth = firstDayOfMonth.endOf('month')
218
+ const today = dayjs().tz(timeZone).startOf('day')
196
219
 
197
- let startOfGrid = getMonday(firstDayOfMonth)
220
+ let startOfGrid = getMonday(firstDayOfMonth, timeZone)
198
221
 
199
- let previousWeekStart = new Date(startOfGrid)
200
- previousWeekStart.setDate(previousWeekStart.getDate() - 7)
201
-
202
- let previousWeekEnd = new Date(startOfGrid)
203
- previousWeekEnd.setDate(previousWeekEnd.getDate() - 1)
222
+ // Previous week range
223
+ const previousWeekStart = startOfGrid.subtract(7, 'day')
224
+ const previousWeekEnd = startOfGrid.subtract(1, 'day')
204
225
 
205
226
  let hadStreakBeforeMonth = false
206
- for (let d = new Date(previousWeekStart); d &lt;= previousWeekEnd; d.setDate(d.getDate() + 1)) {
207
- let dayKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
208
- if (practices[dayKey]) {
227
+ for (let d = previousWeekStart.clone(); d.isSameOrBefore(previousWeekEnd); d = d.add(1, 'day')) {
228
+ const key = d.format('YYYY-MM-DD')
229
+ if (practices[key]) {
209
230
  hadStreakBeforeMonth = true
210
231
  break
211
232
  }
212
233
  }
213
234
 
214
- let endOfMonth = new Date(year, month + 1, 0)
215
- while (endOfMonth.getDay() !== 0) {
216
- endOfMonth.setDate(endOfMonth.getDate() + 1)
235
+ // let endOfMonth = new Date(year, month + 1, 0)
236
+ let endOfGrid = endOfMonth.clone()
237
+ while (endOfGrid.day() !== 0) {
238
+ endOfGrid = endOfGrid.add(1, 'day')
217
239
  }
218
-
219
- let daysInMonth = Math.ceil((endOfMonth - startOfGrid) / (1000 * 60 * 60 * 24)) + 1
220
-
240
+ const daysInMonth = endOfGrid.diff(startOfGrid, 'day') + 1
221
241
  let dailyStats = []
222
242
  let practiceDuration = 0
223
243
  let daysPracticed = 0
224
244
  let weeklyStats = {}
225
245
 
226
246
  for (let i = 0; i &lt; daysInMonth; i++) {
227
- let day = new Date(startOfGrid)
228
- day.setDate(startOfGrid.getDate() + i)
229
- let dayKey = `${day.getFullYear()}-${String(day.getMonth() + 1).padStart(2, '0')}-${String(day.getDate()).padStart(2, '0')}`
230
-
231
- // Check if the user has activity for the day
232
- let dayActivity = practices[dayKey] ?? null
247
+ let day = startOfGrid.add(i, 'day')
248
+ let key = day.format('YYYY-MM-DD')
249
+ let activity = practices[key] ?? null
233
250
  let weekKey = getWeekNumber(day)
234
251
 
235
252
  if (!weeklyStats[weekKey]) {
236
253
  weeklyStats[weekKey] = { key: weekKey, inStreak: false }
237
254
  }
238
255
 
239
- if (dayActivity !== null) {
240
- practiceDuration += dayActivity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
256
+ if (activity &amp;&amp; day.isBetween(firstDayOfMonth, endOfMonth, null, '[]')) {
257
+ practiceDuration += activity.reduce((sum, entry) => sum + entry.duration_seconds, 0)
241
258
  daysPracticed++
242
259
  }
243
260
 
244
- let isActive = isSameDate(today, day)
245
- let type = dayActivity !== null ? 'tracked' : isActive ? 'active' : 'none'
246
- let isInStreak = dayActivity !== null
247
- if (isInStreak) {
261
+ if (activity) {
248
262
  weeklyStats[weekKey].inStreak = true
249
263
  }
250
264
 
265
+ const isActive = day.isSame(today, 'day')
266
+ const type = activity ? 'tracked' : isActive ? 'active' : 'none'
267
+
251
268
  dailyStats.push({
252
269
  key: i,
253
- label: dayKey,
270
+ label: key,
254
271
  isActive,
255
- inStreak: dayActivity !== null,
272
+ inStreak: !!activity,
256
273
  type,
257
274
  })
258
275
  }
259
276
 
277
+ // Continue streak into month
260
278
  if (hadStreakBeforeMonth) {
261
279
  const firstWeekKey = getWeekNumber(startOfGrid)
262
280
  if (weeklyStats[firstWeekKey]) {
@@ -264,14 +282,15 @@ export async function getUserMonthlyStats(params = {}) {
264
282
  }
265
283
  }
266
284
 
267
- let filteredPractices = Object.keys(practices)
268
- .filter((date) => new Date(date) &lt;= endOfMonth)
269
- .reduce((obj, key) => {
270
- obj[key] = practices[key]
271
- return obj
285
+ // Filter past practices only
286
+ let filteredPractices = Object.entries(practices)
287
+ .filter(([date]) => dayjs(date).isSameOrBefore(endOfMonth))
288
+ .reduce((acc, [date, val]) => {
289
+ acc[date] = val
290
+ return acc
272
291
  }, {})
273
292
 
274
- let { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices)
293
+ const { currentDailyStreak, currentWeeklyStreak } = calculateStreaks(filteredPractices)
275
294
 
276
295
  return {
277
296
  data: {
@@ -310,13 +329,16 @@ export async function getUserMonthlyStats(params = {}) {
310
329
  * auto: false,
311
330
  * category_id: 5,
312
331
  * title: "Guitar Warm-up",
313
- * thumbnail_url: "https://example.com/thumbnail.jpg"
332
+ * thumbnail_url: "https://example.com/thumbnail.jpg",
333
+ * instrument_id: 1,
334
+ * instrument_id: 2,
314
335
  * })
315
336
  * .then(response => console.log(response))
316
337
  * .catch(error => console.error(error));
317
338
  */
318
339
  export async function recordUserPractice(practiceDetails) {
319
340
  practiceDetails.auto = 0
341
+ practiceDetails.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
320
342
  if (practiceDetails.content_id) {
321
343
  practiceDetails.auto = 1
322
344
  }
@@ -396,9 +418,14 @@ export async function removeUserPractice(id) {
396
418
  async function (localContext) {
397
419
  if (localContext.data?.[DATA_KEY_PRACTICES]) {
398
420
  Object.keys(localContext.data[DATA_KEY_PRACTICES]).forEach((date) => {
399
- localContext.data[DATA_KEY_PRACTICES][date] = localContext.data[DATA_KEY_PRACTICES][
400
- date
401
- ].filter((practice) => practice.id !== id)
421
+ const filtered = localContext.data[DATA_KEY_PRACTICES][date].filter(
422
+ (practice) => practice.id !== id
423
+ )
424
+ if (filtered.length > 0) {
425
+ localContext.data[DATA_KEY_PRACTICES][date] = filtered
426
+ } else {
427
+ delete localContext.data[DATA_KEY_PRACTICES][date]
428
+ }
402
429
  })
403
430
  }
404
431
  },
@@ -423,20 +450,23 @@ export async function removeUserPractice(id) {
423
450
  export async function restoreUserPractice(id) {
424
451
  let url = `/api/user/practices/v1/practices/restore${buildQueryString([id])}`
425
452
  const response = await fetchHandler(url, 'put')
426
- if (response?.data) {
427
- await userActivityContext.updateLocal(async function (localContext) {
428
- const restoredPractice = response.data
429
- const { date } = restoredPractice
430
- if (!localContext.data[DATA_KEY_PRACTICES][date]) {
431
- localContext.data[DATA_KEY_PRACTICES][date] = []
432
- }
433
- localContext.data[DATA_KEY_PRACTICES][date].push({
434
- id: restoredPractice.id,
435
- duration_seconds: restoredPractice.duration_seconds,
453
+ if (response?.data?.length) {
454
+ const restoredPractice = response.data.find((p) => p.id === id)
455
+ if (restoredPractice) {
456
+ await userActivityContext.updateLocal(async function (localContext) {
457
+ if (!localContext.data[DATA_KEY_PRACTICES][restoredPractice.day]) {
458
+ localContext.data[DATA_KEY_PRACTICES][restoredPractice.day] = []
459
+ }
460
+ response.data.forEach((restoredPractice) => {
461
+ localContext.data[DATA_KEY_PRACTICES][restoredPractice.day].push({
462
+ id: restoredPractice.id,
463
+ duration_seconds: restoredPractice.duration_seconds,
464
+ })
465
+ })
436
466
  })
437
- })
467
+ }
438
468
  }
439
- const formattedMeta = await formatPracticeMeta(response.data)
469
+ const formattedMeta = await formatPracticeMeta(response.data || [])
440
470
  const practiceDuration = formattedMeta.reduce(
441
471
  (total, practice) => total + (practice.duration || 0),
442
472
  0
@@ -598,7 +628,21 @@ export async function getPracticeNotes(day) {
598
628
  * .catch(error => console.error("Failed to get recent activity:", error));
599
629
  */
600
630
  export async function getRecentActivity({ page = 1, limit = 5, tabName = null } = {}) {
601
- return await fetchRecentUserActivities({ page, limit, tabName })
631
+ const recentActivityData = await fetchRecentUserActivities({ page, limit, tabName })
632
+ const contentIds = recentActivityData.data.map((p) => p.contentId).filter((id) => id !== null)
633
+ const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
634
+ addNavigateTo: true,
635
+ addNextLesson: true,
636
+ })
637
+ recentActivityData.data = recentActivityData.data.map((practice) => {
638
+ const content = contents?.find((c) => c.id === practice.contentId) || {}
639
+ return {
640
+ ...practice,
641
+ parent_id: content.parent_id || null,
642
+ navigateTo: content.navigateTo,
643
+ }
644
+ })
645
+ return recentActivityData
602
646
  }
603
647
 
604
648
  /**
@@ -647,10 +691,10 @@ function getStreaksAndMessage(practices) {
647
691
  }
648
692
  }
649
693
 
650
- async function getUserPracticeIds(day = new Date().toISOString().split('T')[0], userId = null) {
694
+ async function getUserPracticeIds(day = dayjs().format('YYYY-MM-DD'), userId = null) {
651
695
  let practices = {}
652
696
  if (userId !== globalConfig.sessionConfig.userId) {
653
- let data = await fetchUserPractices({ userId })
697
+ let data = await fetchUserPractices(0, { userId: userId })
654
698
  practices = data?.['data']?.[DATA_KEY_PRACTICES] ?? {}
655
699
  } else {
656
700
  let data = await userActivityContext.getData()
@@ -677,6 +721,7 @@ function calculateStreaks(practices, includeStreakMessage = false) {
677
721
  let lastActiveDay = null
678
722
  let streakMessage = ''
679
723
 
724
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
680
725
  let sortedPracticeDays = Object.keys(practices)
681
726
  .map((dateStr) => {
682
727
  const [year, month, day] = dateStr.split('-').map(Number)
@@ -728,9 +773,8 @@ function calculateStreaks(practices, includeStreakMessage = false) {
728
773
  let yesterday = new Date(today)
729
774
  yesterday.setDate(today.getDate() - 1)
730
775
 
731
- let currentWeekStart = getMonday(today)
732
- let lastWeekStart = new Date(currentWeekStart)
733
- lastWeekStart.setDate(currentWeekStart.getDate() - 7)
776
+ let currentWeekStart = getMonday(today, timeZone)
777
+ let lastWeekStart = currentWeekStart.subtract(7, 'days')
734
778
 
735
779
  let hasYesterdayPractice = sortedPracticeDays.some((date) => isSameDate(date, yesterday))
736
780
  let hasCurrentWeekPractice = sortedPracticeDays.some((date) => date >= currentWeekStart)
@@ -856,15 +900,15 @@ export async function calculateLongestStreaks(userId = globalConfig.sessionConfi
856
900
  }
857
901
  }
858
902
 
859
- async function formatPracticeMeta(practices) {
903
+ async function formatPracticeMeta(practices = []) {
860
904
  const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
861
- const contents = await fetchByRailContentIds(contentIds)
862
-
863
- const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
905
+ const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
906
+ addNavigateTo: true,
907
+ addNextLesson: true,
908
+ })
864
909
 
865
910
  return practices.map((practice) => {
866
- const utcDate = new Date(practice.created_at)
867
- const content = contents.find((c) => c.id === practice.content_id) || {}
911
+ const content = contents &amp;&amp; contents.length > 0 ? contents.find((c) => c.id === practice.content_id) : {}
868
912
 
869
913
  return {
870
914
  id: practice.id,
@@ -873,19 +917,22 @@ async function formatPracticeMeta(practices) {
873
917
  thumbnail_url: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
874
918
  duration: practice.duration_seconds || 0,
875
919
  duration_seconds: practice.duration_seconds || 0,
876
- content_url: content.url || null,
920
+ content_url: content?.url || null,
877
921
  title: practice.content_id ? content.title : practice.title,
878
922
  category_id: practice.category_id,
879
923
  instrument_id: practice.instrument_id,
880
- content_type: getFormattedType(content.type || '', content.brand),
924
+ content_type: getFormattedType(content?.type || '', content?.brand || null),
881
925
  content_id: practice.content_id || null,
882
- content_brand: content.brand || null,
883
- created_at: convertToTimeZone(utcDate, userTimeZone),
926
+ content_brand: content?.brand || null,
927
+ created_at: dayjs(practice.created_at),
928
+ sanity_type: content?.type || null,
929
+ content_slug: content?.slug || null,
930
+ parent_id: content?.parent_id || null,
931
+ navigateTo: content?.navigateTo || null,
884
932
  }
885
933
  })
886
934
  }
887
935
 
888
-
889
936
  /**
890
937
  * Records a new user activity in the system.
891
938
  *
@@ -929,381 +976,464 @@ export async function deleteUserActivity(id) {
929
976
  const url = `/api/user-management-system/v1/activities/${id}`
930
977
  return await fetchHandler(url, 'DELETE')
931
978
  }
979
+
932
980
  /**
933
- * Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
981
+ * Restores a specific user activity by its ID.
934
982
  *
935
- * @param {Object} [options={}] - Options for fetching progress rows.
936
- * @param {string|null} [options.brand=null] - The brand context for progress data.
937
- * @param {number} [options.limit=8] - Maximum number of progress rows to return.
938
- * @returns {Promise&lt;Object>} - A promise that resolves to an object containing progress rows formatted for UI.
983
+ * @param {number|string} id - The ID of the user activity to restore.
984
+ * @returns {Promise&lt;Object>} - A promise that resolves to the API response after restoration.
939
985
  *
940
986
  * @example
941
- * getProgressRows({ brand: 'drumeo', limit: 10 })
942
- * .then(data => console.log(data))
987
+ * restoreUserActivity(789)
988
+ * .then(response => console.log('Restored:', response))
943
989
  * .catch(error => console.error(error));
944
990
  */
945
- export async function getProgressRows({ brand = null, limit = 8 } = {}) {
991
+ export async function restoreUserActivity(id) {
992
+ const url = `/api/user-management-system/v1/activities/${id}`
993
+ return await fetchHandler(url, 'POST')
994
+ }
995
+
996
+ async function extractPinnedItemsAndSortAllItems(
997
+ userPinnedItem,
998
+ contentsMap,
999
+ eligiblePlaylistItems,
1000
+ limit
1001
+ ) {
1002
+ let pinnedItem = await popPinnedItemFromContentsOrPlaylistMap(
1003
+ userPinnedItem,
1004
+ contentsMap,
1005
+ eligiblePlaylistItems
1006
+ )
1007
+
1008
+ let combined = []
1009
+
1010
+ if (pinnedItem) {
1011
+ pinnedItem.pinned = true
1012
+ combined.push(pinnedItem)
1013
+ }
1014
+
1015
+ const progressList = Array.from(contentsMap.values())
1016
+ combined = [...combined, ...progressList, ...eligiblePlaylistItems]
1017
+ return mergeAndSortItems(combined, limit)
1018
+ }
1019
+
1020
+ function generateContentsMap(contents, playlistsContents) {
946
1021
  const excludedTypes = new Set([
947
1022
  'pack-bundle',
948
1023
  'learning-path-course',
949
- 'learning-path-level'
950
- ]);
951
-
952
- const recentPlaylists = await fetchUserPlaylists(brand, {
953
- sort: '-last_progress',
954
- limit: limit,
955
- });
956
- const playlists = recentPlaylists?.data || [];
957
- const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
958
- const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
959
- const playlistsContents = await fetchByRailContentIds(playlistEngagedOnContents, 'progress-tracker');
960
- const excludedParents = new Set();
961
- const existingShows = new Set();
962
- for (const item of playlistsContents) {
963
- const contentId = item.id ?? item.railcontent_id;
964
- excludedParents.add(contentId)
965
- const parentIds = item.parent_content_data || [];
966
- parentIds.forEach(id => excludedParents.add(id));
967
- }
968
-
969
- const progressContents = await getAllStartedOrCompleted({onlyIds: false, brand: brand, excludedIds: Array.from(excludedParents) });
970
- const contents = await fetchByRailContentIds(Object.keys(progressContents), 'progress-tracker', brand);
971
- const contentsMap = {};
972
- contents.forEach(content => {
973
- contentsMap[content.railcontent_id] = content;
974
- });
975
- const childToParentMap = {};
976
- Object.values(contentsMap).forEach(content => {
1024
+ 'learning-path-level',
1025
+ 'guided-course-part',
1026
+ ])
1027
+ const existingShows = new Set()
1028
+ const contentsMap = new Map()
1029
+ const childToParentMap = {}
1030
+ if (!contents) return contentsMap
1031
+ contents.forEach((content) => {
977
1032
  if (Array.isArray(content.parent_content_data) &amp;&amp; content.parent_content_data.length > 0) {
978
- childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
1033
+ childToParentMap[content.id] =
1034
+ content.parent_content_data[content.parent_content_data.length - 1]
979
1035
  }
980
- });
981
- const progressMap = new Map();
982
- for (const [idStr, progress] of Object.entries(progressContents)) {
983
- const id = parseInt(idStr);
984
- const content = contentsMap[id];
985
- if (!content || excludedTypes.has(content.type)) continue;
986
- const parentId = childToParentMap[id];
987
- // Handle children with parents
988
- if (parentId) {
989
- const parentContent = contentsMap[parentId];
990
- if (!parentContent || excludedTypes.has(parentContent.type)) continue;
991
- const existing = progressMap.get(parentId);
992
- if (existing) {
993
- // If childIndex isn't already set, set it
994
- if (existing.childIndex === undefined) {
995
- existing.childIndex = id;
996
- }
997
- } else {
998
- progressMap.set(parentId, {
999
- id: parentId,
1000
- raw: parentContent,
1001
- state: progress.status,
1002
- percent: progress.progress,
1003
- progressTimestamp: progress.last_update * 1000,
1004
- childIndex: id
1005
- });
1006
- }
1007
- continue;
1008
- }
1009
- // Handle standalone parents
1010
- if (!progressMap.has(id)) {
1011
- if(!existingShows.has(content.type)){
1012
- progressMap.set(id, {
1013
- id,
1014
- raw: content,
1015
- state: progress.status,
1016
- percent: progress.progress,
1017
- progressTimestamp: progress.last_update * 1000
1018
- });
1036
+ })
1037
+
1038
+ const allRecentTypeSet = new Set(Object.values(recentTypes).flat())
1039
+ contents.forEach((content) => {
1040
+ const id = content.id
1041
+ const type = content.type
1042
+ if (
1043
+ excludedTypes.has(type) ||
1044
+ (!allRecentTypeSet.has(type) &amp;&amp; !showsLessonTypes.includes(type))
1045
+ )
1046
+ return
1047
+ if (!childToParentMap[id]) {
1048
+ // Shows don't have a parent to link them, but need to be handled as if they're a set of children
1049
+ if (!existingShows.has(type)) {
1050
+ contentsMap.set(id, content)
1019
1051
  }
1020
- if(showsLessonTypes.includes(content.type)) {
1021
- existingShows.add(content.type)
1052
+ if (showsLessonTypes.includes(type)) {
1053
+ existingShows.add(type)
1022
1054
  }
1023
1055
  }
1024
- }
1025
- const pinnedItem = await extractPinnedItem({
1026
- brand,
1027
- progressMap,
1028
- playlistItems: eligiblePlaylistItems,
1029
1056
  })
1030
- const progressList = Array.from(progressMap.values())
1031
- if (pinnedItem) {
1032
- pinnedItem.pinned = true
1057
+
1058
+ // TODO this doesn't work for guided courses as the GC card takes precedence over the playlist card
1059
+ // https://musora.atlassian.net/browse/BEH-812
1060
+ if (playlistsContents) {
1061
+ for (const item of playlistsContents) {
1062
+ const contentId = item.id
1063
+ contentsMap.delete(contentId)
1064
+ const parentIds = item.parent_content_data || []
1065
+ parentIds.forEach((id) => contentsMap.delete(id))
1066
+ }
1033
1067
  }
1068
+ return contentsMap
1069
+ }
1034
1070
 
1035
- const pinnedId = pinnedItem?.id
1036
- const filteredProgressList = pinnedId
1037
- ? progressList.filter(item => item.id !== pinnedId)
1038
- : progressList;
1039
- const filteredPlaylists = pinnedId
1040
- ? eligiblePlaylistItems.filter(item => item.id !== pinnedId)
1041
- : eligiblePlaylistItems;
1042
- const combinedBase = [...filteredProgressList, ...filteredPlaylists]
1043
- const combined = pinnedItem ? [pinnedItem, ...combinedBase] : combinedBase
1071
+ /**
1072
+ * Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
1073
+ *
1074
+ * @param {Object} [options={}] - Options for fetching progress rows.
1075
+ * @param {string|null} [options.brand=null] - The brand context for progress data.
1076
+ * @param {number} [options.limit=8] - Maximum number of progress rows to return.
1077
+ * @returns {Promise&lt;Object>} - A promise that resolves to an object containing progress rows formatted for UI.
1078
+ *
1079
+ * @example
1080
+ * getProgressRows({ brand: 'drumeo', limit: 10 })
1081
+ * .then(data => console.log(data))
1082
+ * .catch(error => console.error(error));
1083
+ */
1084
+ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
1085
+ // TODO slice progress to a reasonable number, say 100
1086
+
1087
+ const [recentPlaylists, progressContents, userPinnedItem] =
1088
+ await Promise.all([
1089
+ fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit }),
1090
+ getAllStartedOrCompleted({ onlyIds: false, brand: brand }),
1091
+ getUserPinnedItem(brand),
1092
+ ])
1093
+
1094
+ const playlists = recentPlaylists?.data || []
1095
+ const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists)
1096
+ const playlistEngagedOnContents = eligiblePlaylistItems.map(
1097
+ (item) => item.playlist.last_engaged_on
1098
+ )
1044
1099
 
1045
- const finalCombined = mergeAndSortItems(combined, limit)
1100
+ const nonPlaylistContentIds = Object.keys(progressContents)
1101
+ if (userPinnedItem?.progressType === 'content') {
1102
+ nonPlaylistContentIds.push(userPinnedItem.id)
1103
+ }
1046
1104
 
1105
+ const [playlistsContents, contents] = await Promise.all([
1106
+ playlistEngagedOnContents ? addContextToContent(fetchByRailContentIds, playlistEngagedOnContents, 'progress-tracker', {
1107
+ addNextLesson: true,
1108
+ addNavigateTo: true,
1109
+ addProgressStatus: true,
1110
+ addProgressPercentage: true,
1111
+ addProgressTimestamp: true,
1112
+ }) : Promise.resolve([]),
1113
+ nonPlaylistContentIds ? addContextToContent(fetchByRailContentIds, nonPlaylistContentIds, 'progress-tracker', brand, {
1114
+ addNextLesson: true,
1115
+ addNavigateTo: true,
1116
+ addProgressStatus: true,
1117
+ addProgressPercentage: true,
1118
+ addProgressTimestamp: true,
1119
+ }) : Promise.resolve([]),
1120
+ ])
1121
+ const contentsMap = generateContentsMap(contents, playlistsContents)
1122
+ let combined = await extractPinnedItemsAndSortAllItems(
1123
+ userPinnedItem,
1124
+ contentsMap,
1125
+ eligiblePlaylistItems,
1126
+ limit
1127
+ )
1047
1128
  const results = await Promise.all(
1048
- finalCombined.slice(0, limit).map(item =>
1049
- item.type === 'playlist'
1050
- ? processPlaylistItem(item)
1051
- : processContentItem(item)
1052
- )
1053
- );
1054
-
1129
+ combined
1130
+ .slice(0, limit)
1131
+ .map((item) =>
1132
+ item.type === 'playlist' ? processPlaylistItem(item) : processContentItem(item)
1133
+ )
1134
+ )
1055
1135
  return {
1056
1136
  type: TabResponseType.PROGRESS_ROWS,
1057
1137
  displayBrowseAll: combined.length > limit,
1058
- data: results
1059
- };
1138
+ data: results,
1139
+ }
1060
1140
  }
1061
1141
 
1062
- async function processContentItem(item) {
1063
- let data = item.raw;
1064
- const contentType = getFormattedType(data.type, data.brand);
1065
- const status = item.state;
1066
-
1067
- let ctaText = 'Continue';
1068
- if (contentType === 'transcription' || contentType === 'play-along' || contentType === 'jam-track') ctaText = 'Replay Song';
1069
- if (contentType === 'lesson') ctaText = status === 'completed' ? 'Revisit Lesson' : 'Continue';
1070
- if ((contentType === 'guided course' || contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) &amp;&amp; status === 'completed') ctaText = 'Revisit Lessons' ;
1071
- if (contentType === 'pack' &amp;&amp; status === 'completed') {
1072
- ctaText = 'View Lessons';
1073
- }
1142
+ async function getUserPinnedItem(brand) {
1143
+ const userRaw = await globalConfig.localStorage.getItem('user')
1144
+ const user = userRaw ? JSON.parse(userRaw) : {}
1145
+ user.brand_pinned_progress = user.brand_pinned_progress || {}
1146
+ return user.brand_pinned_progress[brand] ?? null
1147
+ }
1074
1148
 
1075
- if (data.lesson_count > 0) {
1076
- const lessonIds = extractLessonIds(item);
1077
- const progressOnItems = await getProgressStateByIds(lessonIds);
1078
- let completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
1079
- data.completed_children = completedCount;
1080
-
1081
- if (item.childIndex) {
1082
- let nextId = item.childIndex
1083
- const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type)
1084
- nextId = nextByProgress ? nextByProgress : nextId
1085
-
1086
- const nestedLessons = data.lessons
1087
- .filter(item => Array.isArray(item.lessons))
1088
- .flatMap(parent =>
1089
- parent.lessons.map(lesson => ({
1090
- ...lesson,
1091
- parent: {
1092
- id: parent.id,
1093
- slug: parent.slug,
1094
- title: parent.title,
1095
- type: parent.type
1096
- }
1097
- }))
1098
- );
1099
-
1100
- const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons
1101
- const nextLesson = lessons.find(lesson => lesson.id === nextId)
1102
- data.first_incomplete_child = nextLesson?.parent ?? nextLesson
1103
- data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null
1104
- if(data.type === 'challenge' &amp;&amp; nextByProgress !== undefined ){
1105
- const challenge = await fetchChallengeLessonData(nextByProgress)
1106
- if(challenge.lesson.is_locked) {
1107
- const timeRemaining = getTimeRemainingUntilLocal(challenge.lesson.unlock_date, {withTotalSeconds:true})
1108
- data.is_locked = true
1109
- data.time_remaining_seconds = timeRemaining.totalSeconds
1110
- ctaText = 'Next lesson in ' + timeRemaining.formatted
1111
- }
1112
- }
1149
+ async function processContentItem(content) {
1150
+ const contentType = getFormattedType(content.type, content.brand)
1151
+ const isLive = content.isLive ?? false
1152
+ let ctaText = getDefaultCTATextForContent(content, contentType)
1153
+
1154
+ content.completed_children = await getCompletedChildren(content, contentType)
1155
+
1156
+ if (content.type === 'guided-course') {
1157
+ const nextLessonPublishedOn = content.children.find(
1158
+ (child) => child.id === content.navigateTo.id
1159
+ )?.published_on
1160
+ let isLocked = new Date(nextLessonPublishedOn) > new Date()
1161
+ if (isLocked) {
1162
+ content.is_locked = true
1163
+ const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {
1164
+ withTotalSeconds: true,
1165
+ })
1166
+ content.time_remaining_seconds = timeRemaining.totalSeconds
1167
+ ctaText = 'Next lesson in ' + timeRemaining.formatted
1168
+ } else if (!content.progressStatus || content.progressStatus === 'not-started' || content.progressPercentage === 0) {
1169
+ ctaText = 'Start Course'
1113
1170
  }
1114
1171
  }
1115
1172
 
1116
- if(contentType == 'show'){
1117
- const shows = await fetchShows(data.brand, data.type)
1118
- const showIds = shows.map(item => item.id);
1119
- const progressOnItems = await getProgressStateByIds(showIds);
1120
- const completedCount = Object.values(progressOnItems).filter(value => value === 'completed').length;
1121
- if(status == 'completed') {
1122
- const nextByProgress = findIncompleteLesson(progressOnItems, data.id, data.type);
1123
- data = shows.find(lesson => lesson.id === nextByProgress);
1173
+ if (contentType === 'show') {
1174
+ const shows = await fetchShows(content.brand, content.type)
1175
+ const showIds = shows.map((item) => item.id)
1176
+ const progressOnItems = await getProgressStateByIds(showIds)
1177
+ const completedShows = content.completed_children
1178
+ const progressTimestamp = content.progressTimestamp
1179
+ const wasPinned = content.pinned ?? false
1180
+ if (content.progressStatus === 'completed') {
1181
+ // this could be handled more gracefully if their was a parent content type for shows
1182
+ const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type)
1183
+ content = shows.find((lesson) => lesson.id === nextByProgress)
1184
+ content.completed_children = completedShows
1185
+ content.progressTimestamp = progressTimestamp
1186
+ content.pinned = wasPinned
1124
1187
  }
1125
- data.completed_children = completedCount;
1126
- data.child_count = shows.length;
1127
- item.percent = Math.round((completedCount / shows.length) * 100);
1128
- if(completedCount == shows.length) {
1129
- ctaText = 'Revisit Lessons';
1188
+ content.child_count = shows.length
1189
+ content.progressPercentage = Math.round((completedShows / shows.length) * 100)
1190
+ if (completedShows === shows.length) {
1191
+ ctaText = 'Revisit Show'
1130
1192
  }
1131
1193
  }
1132
1194
 
1133
1195
  return {
1134
- id: item.id,
1196
+ id: content.id,
1135
1197
  progressType: 'content',
1136
1198
  header: contentType,
1137
- pinned: item.pinned ?? false,
1199
+ pinned: content.pinned ?? false,
1200
+ content: content,
1138
1201
  body: {
1139
- progressPercent: item.percent,
1140
- thumbnail: data.thumbnail,
1141
- title: data.title,
1142
- badge: data.badge ?? null,
1143
- isLocked: data.is_locked ?? false,
1144
- subtitle: !data.child_count || data.lesson_count === 1
1145
- ? (contentType === 'lesson') ? `${item.percent}% Complete`: `${data.difficulty_string} • ${data.artist_name}`
1146
- : `${data.completed_children} of ${data.lesson_count ?? data.child_count} Lessons Complete`
1202
+ progressPercent: isLive ? undefined : content.progressPercentage,
1203
+ thumbnail: content.thumbnail,
1204
+ title: content.title,
1205
+ isLive: isLive,
1206
+ badge: content.badge ?? null,
1207
+ isLocked: content.is_locked ?? false,
1208
+ subtitle: collectionLessonTypes.includes(content.type) || content.lesson_count > 1
1209
+ ? `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
1210
+ : (contentType === 'lesson' &amp;&amp; isLive === false) ? `${content.progressPercentage}% Complete`: `${content.difficulty_string} • ${content.artist_name}`
1147
1211
  },
1148
- cta: {
1149
- text: ctaText,
1150
- timeRemainingToUnlockSeconds: data.time_remaining_seconds ?? null,
1212
+ cta: {
1213
+ text: ctaText,
1214
+ timeRemainingToUnlockSeconds: content.time_remaining_seconds ?? null,
1151
1215
  action: {
1152
- type: data.type,
1153
- brand: data.brand,
1154
- id: data.id,
1155
- slug: data.slug,
1156
- child: data.first_incomplete_child
1157
- ? {
1158
- id: data.first_incomplete_child.id,
1159
- type: data.first_incomplete_child.type,
1160
- brand: data.first_incomplete_child.brand,
1161
- slug: data.first_incomplete_child.slug,
1162
- child: data.second_incomplete_child
1163
- ? {
1164
- id: data.second_incomplete_child.id,
1165
- type: data.second_incomplete_child.type,
1166
- brand: data.second_incomplete_child.brand,
1167
- slug: data.second_incomplete_child.slug
1168
- }
1169
- : null
1170
- }
1171
- : null
1172
- }
1216
+ type: content.type,
1217
+ brand: content.brand,
1218
+ id: content.id,
1219
+ slug: content.slug,
1220
+ child: content.navigateTo,
1221
+ },
1173
1222
  },
1174
- progressTimestamp: item.progressTimestamp
1175
- };
1223
+ // *1000 is to match playlists which are saved in millisecond accuracy
1224
+ progressTimestamp: content.progressTimestamp * 1000,
1225
+ }
1176
1226
  }
1177
1227
 
1178
- async function processPlaylistItem(item) {
1179
- const playlist = item.raw;
1180
- const progressOnItems = await getProgressStateByIds(playlist.items.map(a => a.content_id));
1181
- const allItemsCompleted = item.raw.items.every(i => {
1182
- const itemId = i.content_id;
1183
- const progress = progressOnItems[itemId];
1184
- return progress &amp;&amp; progress === 'completed';
1185
- });
1186
- let nextItem = playlist.items[0] ?? null;
1187
- if (!allItemsCompleted) {
1188
- const lastItemProgress = progressOnItems[playlist.last_engaged_on];
1189
- const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on);
1190
- if (lastItemProgress === 'completed') {
1191
- nextItem = playlist.items[index + 1] ?? nextItem;
1192
- } else {
1193
- nextItem = playlist.items[index] ?? nextItem;
1194
- }
1228
+ function getDefaultCTATextForContent(content, contentType) {
1229
+ let ctaText = 'Continue'
1230
+ if (content.progressStatus === 'completed') {
1231
+ if (
1232
+ contentType === songs[content.brand] ||
1233
+ contentType === 'play along' ||
1234
+ contentType === 'jam track'
1235
+ )
1236
+ ctaText = 'Replay Song'
1237
+ if (contentType === 'lesson') ctaText = 'Revisit Lesson'
1238
+ if (contentType === 'song tutorial' || collectionLessonTypes.includes(content.type))
1239
+ ctaText = 'Revisit Lessons'
1240
+ if (contentType === 'pack') ctaText = 'View Lessons'
1195
1241
  }
1242
+ return ctaText
1243
+ }
1196
1244
 
1245
+ async function getCompletedChildren(content, contentType) {
1246
+ let completedChildren = null
1247
+ if (contentType === 'show') {
1248
+ const shows = await addContextToContent(fetchShows, content.brand, content.type, {
1249
+ addProgressStatus: true,
1250
+ })
1251
+ completedChildren = Object.values(shows).filter(
1252
+ (show) => show.progressStatus === 'completed'
1253
+ ).length
1254
+ } else if (content.lesson_count > 0) {
1255
+ const lessonIds = getLeafNodes(content)
1256
+ const progressOnItems = await getProgressStateByIds(lessonIds)
1257
+ completedChildren = Object.values(progressOnItems).filter(
1258
+ (value) => value === 'completed'
1259
+ ).length
1260
+ }
1261
+ return completedChildren
1262
+ }
1263
+
1264
+ async function processPlaylistItem(item) {
1265
+ const playlist = item.playlist
1197
1266
  return {
1198
- id: playlist.id,
1199
- progressType: 'playlist',
1200
- header: 'playlist',
1201
- pinned: item.pinned ?? false,
1202
- body: {
1267
+ id: playlist.id,
1268
+ progressType: 'playlist',
1269
+ header: 'playlist',
1270
+ pinned: item.pinned ?? false,
1271
+ playlist: playlist,
1272
+ body: {
1203
1273
  first_items_thumbnail_url: playlist.first_items_thumbnail_url,
1204
- title: playlist.name,
1205
- subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
1206
- total_items: playlist.total_items,
1274
+ title: playlist.name,
1275
+ subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
1276
+ total_items: playlist.total_items,
1207
1277
  },
1208
1278
  progressTimestamp: item.progressTimestamp,
1209
- cta: {
1210
- text: 'Continue',
1279
+ cta: {
1280
+ text: 'Continue',
1211
1281
  action: {
1212
- brand: playlist.brand,
1213
- id: playlist.id,
1214
- itemId: nextItem.id,
1215
- type: 'playlists',
1216
- }
1217
- }
1282
+ brand: playlist.brand,
1283
+ item_id: playlist.navigateTo.id ?? null,
1284
+ content_id: playlist.navigateTo.content_id ?? null,
1285
+ type: 'playlists',
1286
+ // TODO depreciated, maintained to avoid breaking changes
1287
+ id: playlist.id,
1288
+ },
1289
+ },
1218
1290
  }
1219
1291
  }
1220
1292
 
1221
1293
  const getFormattedType = (type, brand) => {
1222
1294
  for (const [key, values] of Object.entries(progressTypesMapping)) {
1223
1295
  if (values.includes(type)) {
1224
- return key === 'songs' ? songs[brand] : key;
1296
+ return key === 'songs' ? songs[brand] : key
1225
1297
  }
1226
1298
  }
1227
1299
 
1228
- return null;
1229
- };
1230
-
1231
- function extractLessonIds(data) {
1232
- const ids = [];
1233
- function traverse(lessons) {
1234
- for (const item of lessons) {
1235
- if (item.lessons) {
1236
- traverse(item.lessons); // Recursively handle nested lessons
1237
- }else if (item.id) {
1238
- ids.push(item.id);
1300
+ return null
1301
+ }
1302
+
1303
+ function getLeafNodes(content) {
1304
+ const ids = []
1305
+ function traverse(children) {
1306
+ for (const item of children) {
1307
+ if (item.children) {
1308
+ traverse(item.children) // Recursively handle nested lessons
1309
+ } else if (item.id) {
1310
+ ids.push(item.id)
1239
1311
  }
1240
1312
  }
1241
1313
  }
1242
- if (data.raw &amp;&amp; Array.isArray(data.raw.lessons)) {
1243
- traverse(data.raw.lessons);
1314
+ if (content &amp;&amp; Array.isArray(content.children)) {
1315
+ traverse(content.children)
1244
1316
  }
1245
-
1246
- return ids;
1317
+ return ids
1247
1318
  }
1248
1319
 
1249
-
1250
1320
  async function getEligiblePlaylistItems(playlists) {
1251
- const eligible = playlists.filter(p => p.last_progress &amp;&amp; p.last_engaged_on);
1321
+ const eligible = playlists.filter((p) => p.last_progress &amp;&amp; p.last_engaged_on)
1252
1322
  return Promise.all(
1253
- eligible.map(async p => {
1254
- const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z');
1255
- const timestamp = utcDate.getTime();
1323
+ eligible.map(async (p) => {
1324
+ const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z')
1325
+ const timestamp = utcDate.getTime()
1256
1326
  return {
1257
1327
  type: 'playlist',
1258
- progressTimestamp: timestamp,
1259
- last_engaged_on: p.last_engaged_on,
1260
- raw: p
1261
- };
1328
+ // Content timestamps are millisecond accurate so for comparison we bring this to the same resolution
1329
+ progressTimestamp: timestamp / 1000,
1330
+ playlist: p,
1331
+ id: p.id,
1332
+ }
1262
1333
  })
1263
- );
1334
+ )
1264
1335
  }
1265
1336
 
1266
1337
  function mergeAndSortItems(items, limit) {
1267
- const seen = new Set();
1268
- const deduped = [];
1338
+ const seen = new Set()
1339
+ const deduped = []
1269
1340
 
1270
1341
  for (const item of items) {
1271
- const key = `${item.id}-${item.type || item.raw?.type}`;
1342
+ const key = `${item.id}-${item.type}`
1272
1343
  if (!seen.has(key)) {
1273
- seen.add(key);
1274
- deduped.push(item);
1344
+ seen.add(key)
1345
+ deduped.push(item)
1275
1346
  }
1276
1347
  }
1277
1348
 
1278
1349
  return deduped
1279
- .filter(item => typeof item.progressTimestamp === 'number' &amp;&amp; item.progressTimestamp > 0)
1350
+ .filter((item) => typeof item.progressTimestamp === 'number' &amp;&amp; item.progressTimestamp >= 0)
1280
1351
  .sort((a, b) => {
1281
- if (a.pinned &amp;&amp; !b.pinned) return -1;
1282
- if (!a.pinned &amp;&amp; b.pinned) return 1;
1283
- return b.progressTimestamp - a.progressTimestamp;
1352
+ if (a.pinned &amp;&amp; !b.pinned) return -1
1353
+ if (!a.pinned &amp;&amp; b.pinned) return 1
1354
+ // TODO pinned guided course should always be before user pinned item
1355
+ return b.progressTimestamp - a.progressTimestamp
1284
1356
  })
1285
- .slice(0, limit + 5);
1357
+ .slice(0, limit + 5)
1286
1358
  }
1287
1359
 
1288
- function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1289
- const ids = Object.keys(progressOnItems).map(Number);
1290
- if (contentType === 'challenge') {
1360
+ export function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1361
+ const ids = Object.keys(progressOnItems).map(Number)
1362
+ if (contentType === 'guided-course') {
1291
1363
  // Return first incomplete lesson
1292
- return ids.find(id => progressOnItems[id] !== 'completed') || ids.at(0);
1364
+ return ids.find((id) => progressOnItems[id] !== 'completed') || ids.at(0)
1293
1365
  }
1294
1366
 
1295
1367
  // For other types, find next incomplete after current
1296
- const currentIndex = ids.indexOf(Number(currentContentId));
1297
- if (currentIndex === -1) return null;
1368
+ const currentIndex = ids.indexOf(Number(currentContentId))
1369
+ if (currentIndex === -1) return null
1298
1370
 
1299
1371
  for (let i = currentIndex + 1; i &lt; ids.length; i++) {
1300
- const id = ids[i];
1372
+ const id = ids[i]
1301
1373
  if (progressOnItems[id] !== 'completed') {
1302
- return id;
1374
+ return id
1303
1375
  }
1304
1376
  }
1305
1377
 
1306
- return ids[0];
1378
+ return ids[0]
1379
+ }
1380
+
1381
+ async function popPinnedItemFromContentsOrPlaylistMap(pinned, contentsMap, playlistItems) {
1382
+ if (!pinned) return null
1383
+ const { id, pinnedAt } = pinned
1384
+ let item = null
1385
+ const progressType = pinned.progressType ?? pinned.type;
1386
+
1387
+ if (progressType === 'content') {
1388
+ const pinnedId = parseInt(id)
1389
+ if (contentsMap.has(pinnedId)) {
1390
+ item = contentsMap.get(pinnedId)
1391
+ contentsMap.delete(pinnedId)
1392
+ } else {
1393
+ // we use fetchByRailContentIds so that we don't have the _type restriction in the query
1394
+ let data = await fetchByRailContentIds([id], 'progress-tracker')
1395
+ item = await addContextToContent(() => data[0] ?? null, {
1396
+ addNextLesson: true,
1397
+ addNavigateTo: true,
1398
+ addProgressStatus: true,
1399
+ addProgressPercentage: true,
1400
+ addProgressTimestamp: true,
1401
+ })
1402
+ }
1403
+ }
1404
+ if (progressType === 'playlist') {
1405
+ const pinnedPlaylist = playlistItems.find((p) => p.playlist.id === id)
1406
+ if (pinnedPlaylist) {
1407
+ playlistItems = playlistItems.filter((p) => p.playlist.id !== id)
1408
+ item = pinnedPlaylist
1409
+ } else {
1410
+ const playlist = await fetchPlaylist(id)
1411
+ item = {
1412
+ id: id,
1413
+ playlist: playlist,
1414
+ type: 'playlist',
1415
+ progressTimestamp: new Date(pinnedAt).getTime(),
1416
+ }
1417
+ }
1418
+ }
1419
+ return item
1420
+ }
1421
+
1422
+ function popContentAndRemoveChildrenFromContentsMap(content, contentsMap) {
1423
+ if (!content.children || content.children.length === 0){
1424
+ console.warn(`content ${content.id} has no children`, content);
1425
+ } else {
1426
+ const children = content.children.map((child) => child.id)
1427
+ if (contentsMap.has(content.id)) {
1428
+ contentsMap.delete(content.id)
1429
+ }
1430
+ children.forEach((child) => {
1431
+ if (contentsMap.has(child)) {
1432
+ contentsMap.delete(child)
1433
+ }
1434
+ })
1435
+ }
1436
+ return contentsMap
1307
1437
  }
1308
1438
 
1309
1439
  /**
@@ -1320,16 +1450,16 @@ function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
1320
1450
  * .catch(error => console.error(error));
1321
1451
  */
1322
1452
  export async function pinProgressRow(brand, id, progressType) {
1323
- const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&amp;id=${id}&amp;progressType=${progressType}`;
1453
+ const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&amp;id=${id}&amp;progressType=${progressType}`
1324
1454
  const response = await fetchHandler(url, 'PUT', null)
1325
1455
  if (response &amp;&amp; !response.error) {
1326
- await updatePinnedProgressRow(brand, {
1456
+ await updateUserPinnedProgressRow(brand, {
1327
1457
  id,
1328
1458
  progressType,
1329
1459
  pinnedAt: new Date().toISOString(),
1330
- });
1460
+ })
1331
1461
  }
1332
- return response;
1462
+ return response
1333
1463
  }
1334
1464
  /**
1335
1465
  * Unpins the current pinned progress row for a user, scoped by brand.
@@ -1338,7 +1468,7 @@ export async function pinProgressRow(brand, id, progressType) {
1338
1468
  * @returns {Promise&lt;Object>} - A promise resolving to the response from the unpin API.
1339
1469
  *
1340
1470
  * @example
1341
- * unpinProgressRow('drumeo')
1471
+ * unpinProgressRow('drumeo', 123456)
1342
1472
  * .then(response => console.log(response))
1343
1473
  * .catch(error => console.error(error));
1344
1474
  */
@@ -1346,84 +1476,35 @@ export async function unpinProgressRow(brand) {
1346
1476
  const url = `/api/user-management-system/v1/progress/unpin?brand=${brand}`
1347
1477
  const response = await fetchHandler(url, 'PUT', null)
1348
1478
  if (response &amp;&amp; !response.error) {
1349
- await updatePinnedProgressRow(brand, null)
1479
+ await updateUserPinnedProgressRow(brand, null)
1350
1480
  }
1351
1481
  return response
1352
1482
  }
1353
1483
 
1354
- async function updatePinnedProgressRow(brand, pinnedData) {
1355
- const userRaw = await globalConfig.localStorage.getItem('user');
1356
- const user = userRaw ? JSON.parse(userRaw) : {};
1484
+ async function updateUserPinnedProgressRow(brand, pinnedData) {
1485
+ const userRaw = await globalConfig.localStorage.getItem('user')
1486
+ const user = userRaw ? JSON.parse(userRaw) : {}
1357
1487
  user.brand_pinned_progress = user.brand_pinned_progress || {}
1358
1488
  user.brand_pinned_progress[brand] = pinnedData
1359
1489
  await globalConfig.localStorage.setItem('user', JSON.stringify(user))
1360
1490
  }
1361
1491
 
1362
- async function extractPinnedItem({brand, progressMap, playlistItems}) {
1363
- const userRaw = await globalConfig.localStorage.getItem('user');
1364
- const user = userRaw ? JSON.parse(userRaw) : {};
1365
- user.brand_pinned_progress = user.brand_pinned_progress || {}
1366
-
1367
- const pinned = user.brand_pinned_progress[brand]
1368
- if (!pinned) return null
1369
-
1370
- const {id, progressType, pinnedAt} = pinned
1492
+ export async function fetchRecentActivitiesActiveTabs() {
1493
+ const url = `/api/user-management-system/v1/activities/tabs`
1494
+ try {
1495
+ const tabs = await fetchHandler(url, 'GET')
1496
+ const activitiesTabs = []
1371
1497
 
1372
- if (progressType === 'content') {
1373
- const pinnedId = parseInt(id)
1374
- if (progressMap.has(pinnedId)) {
1375
- const item = progressMap.get(pinnedId)
1376
- progressMap.delete(pinnedId)
1377
- return item
1378
- } else {
1379
- const content = await fetchByRailContentIds([`${pinnedId}`], 'progress-tracker')
1380
- const firstLessonId = getFirstLeafLessonId(content[0])
1381
- return {
1382
- id: pinnedId,
1383
- state: 'started',
1384
- percent: 0,
1385
- raw: content[0],
1386
- progressTimestamp: new Date(pinnedAt).getTime(),
1387
- childIndex: firstLessonId
1388
- }
1389
- }
1390
- }
1391
- if (progressType === 'playlist') {
1392
- const pinnedPlaylist = playlistItems.find(p => p.raw.id === id)
1393
- if (pinnedPlaylist) {
1394
- return pinnedPlaylist
1395
- }else{
1396
- const playlist = await fetchPlaylist(id)
1397
- return {
1398
- id: id,
1399
- raw: playlist,
1400
- progressTimestamp: new Date(pinnedAt).getTime(),
1401
- type: 'playlist',
1402
- last_engaged_on: playlist.items[0],
1403
- }
1404
- }
1405
- }
1406
-
1407
- return null
1408
- }
1498
+ tabs.forEach((tab) => {
1499
+ activitiesTabs.push({ name: tab.label, short_name: tab.label })
1500
+ })
1409
1501
 
1410
- function getFirstLeafLessonId(data) {
1411
- function findFirstLeaf(lessons) {
1412
- for (const item of lessons) {
1413
- if (!item.lessons || item.lessons.length === 0) {
1414
- return item.id || null
1415
- }
1416
- const found = findFirstLeaf(item.lessons)
1417
- if (found) return found
1418
- }
1419
- return null
1502
+ return activitiesTabs
1503
+ } catch (error) {
1504
+ console.error('Error fetching activity tabs:', error)
1505
+ return []
1420
1506
  }
1421
-
1422
- return data.lessons ? findFirstLeaf(data.lessons) : null
1423
1507
  }
1424
-
1425
-
1426
-
1427
1508
  </code></pre>
1428
1509
  </article>
1429
1510
  </section>
@@ -1438,7 +1519,7 @@ function getFirstLeafLessonId(data) {
1438
1519
  <br class="clear">
1439
1520
 
1440
1521
  <footer>
1441
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.3</a> on Mon Jun 16 2025 14:26:22 GMT+0300 (Eastern European Summer Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1522
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.3</a> on Tue Nov 04 2025 18:21:55 GMT+0000 (Coordinated Universal Time) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
1442
1523
  </footer>
1443
1524
 
1444
1525
  <script>prettyPrint();</script>