musora-content-services 2.30.4 → 2.30.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/services/userActivity.js +175 -145
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.30.5](https://github.com/railroadmedia/musora-content-services/compare/v2.30.4...v2.30.5) (2025-08-06)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **MU2-918:** practice meta data for manual only sessions ([#402](https://github.com/railroadmedia/musora-content-services/issues/402)) ([a911529](https://github.com/railroadmedia/musora-content-services/commit/a9115297f00b6aed5f0ad73714cb860cfd71f331))
|
|
11
|
+
* **MU2-918:** safer .find use ([#405](https://github.com/railroadmedia/musora-content-services/issues/405)) ([ce81469](https://github.com/railroadmedia/musora-content-services/commit/ce814692a7aa46ee814c28ecae1ec404a0c09046))
|
|
12
|
+
|
|
5
13
|
### [2.30.4](https://github.com/railroadmedia/musora-content-services/compare/v2.30.3...v2.30.4) (2025-08-05)
|
|
6
14
|
|
|
7
15
|
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ import { TabResponseType } from '../contentMetaData'
|
|
|
35
35
|
import dayjs from 'dayjs'
|
|
36
36
|
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
|
37
37
|
import weekOfYear from 'dayjs/plugin/weekOfYear'
|
|
38
|
-
import {addContextToContent} from
|
|
38
|
+
import { addContextToContent } from './contentAggregator.js'
|
|
39
39
|
|
|
40
40
|
const DATA_KEY_PRACTICES = 'practices'
|
|
41
41
|
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
@@ -585,10 +585,10 @@ export async function getRecentActivity({ page = 1, limit = 5, tabName = null }
|
|
|
585
585
|
const contentIds = recentActivityData.data.map((p) => p.contentId).filter((id) => id !== null)
|
|
586
586
|
const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
|
|
587
587
|
addNavigateTo: true,
|
|
588
|
-
addNextLesson: true
|
|
588
|
+
addNextLesson: true,
|
|
589
589
|
})
|
|
590
590
|
recentActivityData.data = recentActivityData.data.map((practice) => {
|
|
591
|
-
const content = contents
|
|
591
|
+
const content = contents?.find((c) => c.id === practice.contentId) || {}
|
|
592
592
|
return {
|
|
593
593
|
...practice,
|
|
594
594
|
parent_id: content.parent_id || null,
|
|
@@ -853,14 +853,15 @@ export async function calculateLongestStreaks(userId = globalConfig.sessionConfi
|
|
|
853
853
|
}
|
|
854
854
|
}
|
|
855
855
|
|
|
856
|
-
async function formatPracticeMeta(practices) {
|
|
856
|
+
async function formatPracticeMeta(practices = []) {
|
|
857
857
|
const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
|
|
858
858
|
const contents = await addContextToContent(fetchByRailContentIds, contentIds, {
|
|
859
859
|
addNavigateTo: true,
|
|
860
|
-
addNextLesson: true
|
|
860
|
+
addNextLesson: true,
|
|
861
861
|
})
|
|
862
|
+
|
|
862
863
|
return practices.map((practice) => {
|
|
863
|
-
const content = contents.find((c) => c.id === practice.content_id)
|
|
864
|
+
const content = contents && contents.length > 0 ? contents.find((c) => c.id === practice.content_id) : {}
|
|
864
865
|
|
|
865
866
|
return {
|
|
866
867
|
id: practice.id,
|
|
@@ -869,18 +870,18 @@ async function formatPracticeMeta(practices) {
|
|
|
869
870
|
thumbnail_url: practice.content_id ? content.thumbnail : practice.thumbnail_url || '',
|
|
870
871
|
duration: practice.duration_seconds || 0,
|
|
871
872
|
duration_seconds: practice.duration_seconds || 0,
|
|
872
|
-
content_url: content
|
|
873
|
+
content_url: content?.url || null,
|
|
873
874
|
title: practice.content_id ? content.title : practice.title,
|
|
874
875
|
category_id: practice.category_id,
|
|
875
876
|
instrument_id: practice.instrument_id,
|
|
876
|
-
content_type: getFormattedType(content
|
|
877
|
+
content_type: getFormattedType(content?.type || '', content?.brand || null),
|
|
877
878
|
content_id: practice.content_id || null,
|
|
878
|
-
content_brand: content
|
|
879
|
+
content_brand: content?.brand || null,
|
|
879
880
|
created_at: dayjs(practice.created_at),
|
|
880
|
-
sanity_type: content
|
|
881
|
-
content_slug: content
|
|
882
|
-
parent_id: content
|
|
883
|
-
navigateTo: content
|
|
881
|
+
sanity_type: content?.type || null,
|
|
882
|
+
content_slug: content?.slug || null,
|
|
883
|
+
parent_id: content?.parent_id || null,
|
|
884
|
+
navigateTo: content?.navigateTo || null,
|
|
884
885
|
}
|
|
885
886
|
})
|
|
886
887
|
}
|
|
@@ -929,24 +930,31 @@ export async function deleteUserActivity(id) {
|
|
|
929
930
|
return await fetchHandler(url, 'DELETE')
|
|
930
931
|
}
|
|
931
932
|
|
|
932
|
-
async function extractPinnedItemsAndSortAllItems(
|
|
933
|
+
async function extractPinnedItemsAndSortAllItems(
|
|
934
|
+
userPinnedItem,
|
|
935
|
+
contentsMap,
|
|
936
|
+
eligiblePlaylistItems,
|
|
937
|
+
pinnedGuidedCourse,
|
|
938
|
+
limit
|
|
939
|
+
) {
|
|
933
940
|
let pinnedItem = await popPinnedItemFromContentsOrPlaylistMap(
|
|
934
941
|
userPinnedItem,
|
|
935
942
|
contentsMap,
|
|
936
|
-
eligiblePlaylistItems
|
|
943
|
+
eligiblePlaylistItems
|
|
937
944
|
)
|
|
938
945
|
|
|
939
946
|
const guidedCourseID = pinnedGuidedCourse?.content_id
|
|
940
|
-
let combined = []
|
|
947
|
+
let combined = []
|
|
941
948
|
if (pinnedGuidedCourse) {
|
|
942
|
-
const guidedCourseContent =
|
|
943
|
-
|
|
949
|
+
const guidedCourseContent =
|
|
950
|
+
contentsMap.get(guidedCourseID) ??
|
|
951
|
+
(await addContextToContent(fetchByRailContentId, guidedCourseID, 'guided-course', {
|
|
944
952
|
addNextLesson: true,
|
|
945
953
|
addNavigateTo: true,
|
|
946
954
|
addProgressStatus: true,
|
|
947
955
|
addProgressPercentage: true,
|
|
948
956
|
addProgressTimestamp: true,
|
|
949
|
-
})
|
|
957
|
+
}))
|
|
950
958
|
contentsMap = popContentAndRemoveChildrenFromContentsMap(guidedCourseContent, contentsMap)
|
|
951
959
|
guidedCourseContent.pinned = true
|
|
952
960
|
combined.push(guidedCourseContent)
|
|
@@ -967,23 +975,26 @@ function generateContentsMap(contents, playlistsContents) {
|
|
|
967
975
|
'learning-path-course',
|
|
968
976
|
'learning-path-level',
|
|
969
977
|
'guided-course-part',
|
|
970
|
-
])
|
|
971
|
-
const existingShows = new Set()
|
|
972
|
-
const contentsMap = new Map()
|
|
973
|
-
const childToParentMap = {}
|
|
974
|
-
contents.forEach(content => {
|
|
978
|
+
])
|
|
979
|
+
const existingShows = new Set()
|
|
980
|
+
const contentsMap = new Map()
|
|
981
|
+
const childToParentMap = {}
|
|
982
|
+
contents.forEach((content) => {
|
|
975
983
|
if (Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0) {
|
|
976
|
-
childToParentMap[content.id] =
|
|
984
|
+
childToParentMap[content.id] =
|
|
985
|
+
content.parent_content_data[content.parent_content_data.length - 1]
|
|
977
986
|
}
|
|
978
|
-
})
|
|
987
|
+
})
|
|
979
988
|
|
|
980
|
-
const allRecentTypeSet = new Set(
|
|
981
|
-
|
|
982
|
-
)
|
|
983
|
-
contents.forEach(content => {
|
|
989
|
+
const allRecentTypeSet = new Set(Object.values(recentTypes).flat())
|
|
990
|
+
contents.forEach((content) => {
|
|
984
991
|
const id = content.id
|
|
985
992
|
const type = content.type
|
|
986
|
-
if (
|
|
993
|
+
if (
|
|
994
|
+
excludedTypes.has(type) ||
|
|
995
|
+
(!allRecentTypeSet.has(type) && !showsLessonTypes.includes(type))
|
|
996
|
+
)
|
|
997
|
+
return
|
|
987
998
|
if (!childToParentMap[id]) {
|
|
988
999
|
// Shows don't have a parent to link them, but need to be handled as if they're a set of children
|
|
989
1000
|
if (!existingShows.has(type)) {
|
|
@@ -1001,11 +1012,11 @@ function generateContentsMap(contents, playlistsContents) {
|
|
|
1001
1012
|
for (const item of playlistsContents) {
|
|
1002
1013
|
const contentId = item.id
|
|
1003
1014
|
contentsMap.delete(contentId)
|
|
1004
|
-
const parentIds = item.parent_content_data || []
|
|
1005
|
-
parentIds.forEach(id => contentsMap.delete(id))
|
|
1015
|
+
const parentIds = item.parent_content_data || []
|
|
1016
|
+
parentIds.forEach((id) => contentsMap.delete(id))
|
|
1006
1017
|
}
|
|
1007
1018
|
}
|
|
1008
|
-
return contentsMap
|
|
1019
|
+
return contentsMap
|
|
1009
1020
|
}
|
|
1010
1021
|
|
|
1011
1022
|
/**
|
|
@@ -1022,19 +1033,21 @@ function generateContentsMap(contents, playlistsContents) {
|
|
|
1022
1033
|
* .catch(error => console.error(error));
|
|
1023
1034
|
*/
|
|
1024
1035
|
export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
1025
|
-
|
|
1026
1036
|
// TODO slice progress to a reasonable number, say 100
|
|
1027
|
-
const [recentPlaylists, progressContents, allPinnedGuidedCourse, userPinnedItem
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1037
|
+
const [recentPlaylists, progressContents, allPinnedGuidedCourse, userPinnedItem] =
|
|
1038
|
+
await Promise.all([
|
|
1039
|
+
fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit }),
|
|
1040
|
+
getAllStartedOrCompleted({ onlyIds: false, brand: brand }),
|
|
1041
|
+
pinnedGuidedCourses(brand),
|
|
1042
|
+
getUserPinnedItem(brand),
|
|
1043
|
+
])
|
|
1033
1044
|
let pinnedGuidedCourse = allPinnedGuidedCourse?.[0] ?? null
|
|
1034
1045
|
|
|
1035
|
-
const playlists = recentPlaylists?.data || []
|
|
1036
|
-
const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists)
|
|
1037
|
-
const playlistEngagedOnContents = eligiblePlaylistItems.map(
|
|
1046
|
+
const playlists = recentPlaylists?.data || []
|
|
1047
|
+
const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists)
|
|
1048
|
+
const playlistEngagedOnContents = eligiblePlaylistItems.map(
|
|
1049
|
+
(item) => item.playlist.last_engaged_on
|
|
1050
|
+
)
|
|
1038
1051
|
|
|
1039
1052
|
const nonPlaylistContentIds = Object.keys(progressContents)
|
|
1040
1053
|
if (pinnedGuidedCourse) {
|
|
@@ -1043,7 +1056,7 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
1043
1056
|
if (userPinnedItem?.progressType === 'content') {
|
|
1044
1057
|
nonPlaylistContentIds.push(userPinnedItem.id)
|
|
1045
1058
|
}
|
|
1046
|
-
const [
|
|
1059
|
+
const [playlistsContents, contents] = await Promise.all([
|
|
1047
1060
|
addContextToContent(fetchByRailContentIds, playlistEngagedOnContents, 'progress-tracker', {
|
|
1048
1061
|
addNextLesson: true,
|
|
1049
1062
|
addNavigateTo: true,
|
|
@@ -1057,17 +1070,23 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
1057
1070
|
addProgressStatus: true,
|
|
1058
1071
|
addProgressPercentage: true,
|
|
1059
1072
|
addProgressTimestamp: true,
|
|
1060
|
-
})
|
|
1061
|
-
])
|
|
1062
|
-
const contentsMap = generateContentsMap(contents, playlistsContents)
|
|
1063
|
-
let combined = await extractPinnedItemsAndSortAllItems(
|
|
1073
|
+
}),
|
|
1074
|
+
])
|
|
1075
|
+
const contentsMap = generateContentsMap(contents, playlistsContents)
|
|
1076
|
+
let combined = await extractPinnedItemsAndSortAllItems(
|
|
1077
|
+
userPinnedItem,
|
|
1078
|
+
contentsMap,
|
|
1079
|
+
eligiblePlaylistItems,
|
|
1080
|
+
pinnedGuidedCourse,
|
|
1081
|
+
limit
|
|
1082
|
+
)
|
|
1064
1083
|
const results = await Promise.all(
|
|
1065
|
-
combined
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
: processContentItem(item)
|
|
1069
|
-
|
|
1070
|
-
)
|
|
1084
|
+
combined
|
|
1085
|
+
.slice(0, limit)
|
|
1086
|
+
.map((item) =>
|
|
1087
|
+
item.type === 'playlist' ? processPlaylistItem(item) : processContentItem(item)
|
|
1088
|
+
)
|
|
1089
|
+
)
|
|
1071
1090
|
console.log('HomePageProgressRows results: remove before merge', results)
|
|
1072
1091
|
return {
|
|
1073
1092
|
type: TabResponseType.PROGRESS_ROWS,
|
|
@@ -1084,118 +1103,132 @@ async function getUserPinnedItem(brand) {
|
|
|
1084
1103
|
}
|
|
1085
1104
|
|
|
1086
1105
|
async function processContentItem(content) {
|
|
1087
|
-
const contentType = getFormattedType(content.type, content.brand)
|
|
1106
|
+
const contentType = getFormattedType(content.type, content.brand)
|
|
1088
1107
|
const isLive = content.isLive ?? false
|
|
1089
1108
|
let ctaText = getDefaultCTATextForContent(content, contentType)
|
|
1090
1109
|
|
|
1091
1110
|
content.completed_children = await getCompletedChildren(content, contentType)
|
|
1092
1111
|
|
|
1093
1112
|
if (content.type === 'guided-course') {
|
|
1094
|
-
const nextLessonPublishedOn = content.children.find(
|
|
1113
|
+
const nextLessonPublishedOn = content.children.find(
|
|
1114
|
+
(child) => child.id === content.navigateTo.id
|
|
1115
|
+
)?.published_on
|
|
1095
1116
|
let isLocked = new Date(nextLessonPublishedOn) > new Date()
|
|
1096
1117
|
if (isLocked) {
|
|
1097
1118
|
content.is_locked = true
|
|
1098
|
-
const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {
|
|
1119
|
+
const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {
|
|
1120
|
+
withTotalSeconds: true,
|
|
1121
|
+
})
|
|
1099
1122
|
content.time_remaining_seconds = timeRemaining.totalSeconds
|
|
1100
1123
|
ctaText = 'Next lesson in ' + timeRemaining.formatted
|
|
1101
|
-
} else if (!content.progressStatus || content.progressStatus === 'not-started'
|
|
1102
|
-
ctaText =
|
|
1124
|
+
} else if (!content.progressStatus || content.progressStatus === 'not-started') {
|
|
1125
|
+
ctaText = 'Start Course'
|
|
1103
1126
|
}
|
|
1104
1127
|
}
|
|
1105
1128
|
|
|
1106
|
-
if (contentType === 'show'){
|
|
1129
|
+
if (contentType === 'show') {
|
|
1107
1130
|
const shows = await fetchShows(content.brand, content.type)
|
|
1108
|
-
const showIds = shows.map(item => item.id)
|
|
1109
|
-
const progressOnItems = await getProgressStateByIds(showIds)
|
|
1131
|
+
const showIds = shows.map((item) => item.id)
|
|
1132
|
+
const progressOnItems = await getProgressStateByIds(showIds)
|
|
1110
1133
|
const completedShows = content.completed_children
|
|
1111
1134
|
const progressTimestamp = content.progressTimestamp
|
|
1112
1135
|
const wasPinned = content.pinned ?? false
|
|
1113
1136
|
if (content.progressStatus === 'completed') {
|
|
1114
1137
|
// this could be handled more gracefully if their was a parent content type for shows
|
|
1115
|
-
const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type)
|
|
1116
|
-
content = shows.find(lesson => lesson.id === nextByProgress)
|
|
1138
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type)
|
|
1139
|
+
content = shows.find((lesson) => lesson.id === nextByProgress)
|
|
1117
1140
|
content.completed_children = completedShows
|
|
1118
1141
|
content.progressTimestamp = progressTimestamp
|
|
1119
1142
|
content.pinned = wasPinned
|
|
1120
1143
|
}
|
|
1121
|
-
content.child_count = shows.length
|
|
1122
|
-
content.progressPercentage = Math.round((completedShows / shows.length) * 100)
|
|
1144
|
+
content.child_count = shows.length
|
|
1145
|
+
content.progressPercentage = Math.round((completedShows / shows.length) * 100)
|
|
1123
1146
|
if (completedShows === shows.length) {
|
|
1124
|
-
ctaText = 'Revisit Show'
|
|
1147
|
+
ctaText = 'Revisit Show'
|
|
1125
1148
|
}
|
|
1126
1149
|
}
|
|
1127
1150
|
|
|
1128
1151
|
return {
|
|
1129
|
-
id:
|
|
1130
|
-
progressType:
|
|
1131
|
-
header:
|
|
1132
|
-
pinned:
|
|
1133
|
-
content:
|
|
1134
|
-
body:
|
|
1135
|
-
progressPercent: isLive ? undefined: content.progressPercentage,
|
|
1136
|
-
thumbnail:
|
|
1137
|
-
title:
|
|
1138
|
-
isLive:
|
|
1139
|
-
badge:
|
|
1140
|
-
isLocked:
|
|
1141
|
-
subtitle:
|
|
1142
|
-
|
|
1143
|
-
|
|
1152
|
+
id: content.id,
|
|
1153
|
+
progressType: 'content',
|
|
1154
|
+
header: contentType,
|
|
1155
|
+
pinned: content.pinned ?? false,
|
|
1156
|
+
content: content,
|
|
1157
|
+
body: {
|
|
1158
|
+
progressPercent: isLive ? undefined : content.progressPercentage,
|
|
1159
|
+
thumbnail: content.thumbnail,
|
|
1160
|
+
title: content.title,
|
|
1161
|
+
isLive: isLive,
|
|
1162
|
+
badge: content.badge ?? null,
|
|
1163
|
+
isLocked: content.is_locked ?? false,
|
|
1164
|
+
subtitle:
|
|
1165
|
+
!content.child_count || content.lesson_count === 1
|
|
1166
|
+
? contentType === 'lesson' && isLive === false
|
|
1167
|
+
? `${content.progressPercentage}% Complete`
|
|
1168
|
+
: `${content.difficulty_string} • ${content.artist_name}`
|
|
1169
|
+
: `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`,
|
|
1144
1170
|
},
|
|
1145
|
-
cta:
|
|
1146
|
-
text:
|
|
1171
|
+
cta: {
|
|
1172
|
+
text: ctaText,
|
|
1147
1173
|
timeRemainingToUnlockSeconds: content.time_remaining_seconds ?? null,
|
|
1148
1174
|
action: {
|
|
1149
|
-
type:
|
|
1175
|
+
type: content.type,
|
|
1150
1176
|
brand: content.brand,
|
|
1151
|
-
id:
|
|
1152
|
-
slug:
|
|
1177
|
+
id: content.id,
|
|
1178
|
+
slug: content.slug,
|
|
1153
1179
|
child: content.navigateTo,
|
|
1154
|
-
}
|
|
1180
|
+
},
|
|
1155
1181
|
},
|
|
1156
1182
|
// *1000 is to match playlists which are saved in millisecond accuracy
|
|
1157
|
-
progressTimestamp: content.progressTimestamp * 1000
|
|
1158
|
-
}
|
|
1183
|
+
progressTimestamp: content.progressTimestamp * 1000,
|
|
1184
|
+
}
|
|
1159
1185
|
}
|
|
1160
1186
|
|
|
1161
|
-
function getDefaultCTATextForContent(content, contentType)
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1187
|
+
function getDefaultCTATextForContent(content, contentType) {
|
|
1188
|
+
let ctaText = 'Continue'
|
|
1189
|
+
if (content.progressStatus === 'completed') {
|
|
1190
|
+
if (
|
|
1191
|
+
contentType === songs[content.brand] ||
|
|
1192
|
+
contentType === 'play along' ||
|
|
1193
|
+
contentType === 'jam track'
|
|
1194
|
+
)
|
|
1195
|
+
ctaText = 'Replay Song'
|
|
1196
|
+
if (contentType === 'lesson') ctaText = 'Revisit Lesson'
|
|
1197
|
+
if (contentType === 'song tutorial' || collectionLessonTypes.includes(contentType))
|
|
1198
|
+
ctaText = 'Revisit Lessons'
|
|
1169
1199
|
if (contentType === 'pack') ctaText = 'View Lessons'
|
|
1170
1200
|
}
|
|
1171
1201
|
return ctaText
|
|
1172
1202
|
}
|
|
1173
1203
|
|
|
1174
|
-
async function getCompletedChildren(content, contentType)
|
|
1175
|
-
{
|
|
1204
|
+
async function getCompletedChildren(content, contentType) {
|
|
1176
1205
|
let completedChildren = null
|
|
1177
1206
|
if (contentType === 'show') {
|
|
1178
1207
|
const shows = await addContextToContent(fetchShows, content.brand, content.type, {
|
|
1179
1208
|
addProgressStatus: true,
|
|
1180
1209
|
})
|
|
1181
|
-
completedChildren = Object.values(shows).filter(
|
|
1210
|
+
completedChildren = Object.values(shows).filter(
|
|
1211
|
+
(show) => show.progressStatus === 'completed'
|
|
1212
|
+
).length
|
|
1182
1213
|
} else if (content.lesson_count > 0) {
|
|
1183
|
-
const lessonIds = getLeafNodes(content)
|
|
1184
|
-
const progressOnItems = await getProgressStateByIds(lessonIds)
|
|
1185
|
-
completedChildren
|
|
1214
|
+
const lessonIds = getLeafNodes(content)
|
|
1215
|
+
const progressOnItems = await getProgressStateByIds(lessonIds)
|
|
1216
|
+
completedChildren = Object.values(progressOnItems).filter(
|
|
1217
|
+
(value) => value === 'completed'
|
|
1218
|
+
).length
|
|
1186
1219
|
}
|
|
1187
1220
|
return completedChildren
|
|
1188
1221
|
}
|
|
1189
1222
|
|
|
1190
1223
|
async function processPlaylistItem(item) {
|
|
1191
|
-
const playlist = item.playlist
|
|
1224
|
+
const playlist = item.playlist
|
|
1192
1225
|
return {
|
|
1193
|
-
id:
|
|
1194
|
-
progressType:
|
|
1195
|
-
header:
|
|
1196
|
-
pinned:
|
|
1197
|
-
playlist:
|
|
1198
|
-
body:
|
|
1226
|
+
id: playlist.id,
|
|
1227
|
+
progressType: 'playlist',
|
|
1228
|
+
header: 'playlist',
|
|
1229
|
+
pinned: item.pinned ?? false,
|
|
1230
|
+
playlist: playlist,
|
|
1231
|
+
body: {
|
|
1199
1232
|
first_items_thumbnail_url: playlist.first_items_thumbnail_url,
|
|
1200
1233
|
title: playlist.name,
|
|
1201
1234
|
subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
|
|
@@ -1205,14 +1238,14 @@ async function processPlaylistItem(item) {
|
|
|
1205
1238
|
cta: {
|
|
1206
1239
|
text: 'Continue',
|
|
1207
1240
|
action: {
|
|
1208
|
-
brand:
|
|
1241
|
+
brand: playlist.brand,
|
|
1209
1242
|
item_id: playlist.navigateTo.id ?? null,
|
|
1210
1243
|
content_id: playlist.navigateTo.content_id ?? null,
|
|
1211
|
-
type:
|
|
1244
|
+
type: 'playlists',
|
|
1212
1245
|
// TODO depreciated, maintained to avoid breaking changes
|
|
1213
|
-
id:
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1246
|
+
id: playlist.id,
|
|
1247
|
+
},
|
|
1248
|
+
},
|
|
1216
1249
|
}
|
|
1217
1250
|
}
|
|
1218
1251
|
|
|
@@ -1227,18 +1260,18 @@ const getFormattedType = (type, brand) => {
|
|
|
1227
1260
|
}
|
|
1228
1261
|
|
|
1229
1262
|
function getLeafNodes(content) {
|
|
1230
|
-
const ids = []
|
|
1263
|
+
const ids = []
|
|
1231
1264
|
function traverse(children) {
|
|
1232
1265
|
for (const item of children) {
|
|
1233
1266
|
if (item.children) {
|
|
1234
|
-
traverse(item.children)
|
|
1267
|
+
traverse(item.children) // Recursively handle nested lessons
|
|
1235
1268
|
} else if (item.id) {
|
|
1236
|
-
ids.push(item.id)
|
|
1269
|
+
ids.push(item.id)
|
|
1237
1270
|
}
|
|
1238
1271
|
}
|
|
1239
1272
|
}
|
|
1240
1273
|
if (content && Array.isArray(content.children)) {
|
|
1241
|
-
traverse(content.children)
|
|
1274
|
+
traverse(content.children)
|
|
1242
1275
|
}
|
|
1243
1276
|
return ids
|
|
1244
1277
|
}
|
|
@@ -1255,7 +1288,7 @@ async function getEligiblePlaylistItems(playlists) {
|
|
|
1255
1288
|
progressTimestamp: timestamp / 1000,
|
|
1256
1289
|
playlist: p,
|
|
1257
1290
|
id: p.id,
|
|
1258
|
-
}
|
|
1291
|
+
}
|
|
1259
1292
|
})
|
|
1260
1293
|
)
|
|
1261
1294
|
}
|
|
@@ -1265,7 +1298,7 @@ function mergeAndSortItems(items, limit) {
|
|
|
1265
1298
|
const deduped = []
|
|
1266
1299
|
|
|
1267
1300
|
for (const item of items) {
|
|
1268
|
-
const key = `${item.id}-${item.type}
|
|
1301
|
+
const key = `${item.id}-${item.type}`
|
|
1269
1302
|
if (!seen.has(key)) {
|
|
1270
1303
|
seen.add(key)
|
|
1271
1304
|
deduped.push(item)
|
|
@@ -1273,12 +1306,12 @@ function mergeAndSortItems(items, limit) {
|
|
|
1273
1306
|
}
|
|
1274
1307
|
|
|
1275
1308
|
return deduped
|
|
1276
|
-
.filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp >= 0)
|
|
1309
|
+
.filter((item) => typeof item.progressTimestamp === 'number' && item.progressTimestamp >= 0)
|
|
1277
1310
|
.sort((a, b) => {
|
|
1278
|
-
if (a.pinned && !b.pinned) return -1
|
|
1279
|
-
if (!a.pinned && b.pinned) return 1
|
|
1311
|
+
if (a.pinned && !b.pinned) return -1
|
|
1312
|
+
if (!a.pinned && b.pinned) return 1
|
|
1280
1313
|
// TODO pinned guided course should always be before user pinned item
|
|
1281
|
-
return b.progressTimestamp - a.progressTimestamp
|
|
1314
|
+
return b.progressTimestamp - a.progressTimestamp
|
|
1282
1315
|
})
|
|
1283
1316
|
.slice(0, limit + 5)
|
|
1284
1317
|
}
|
|
@@ -1306,39 +1339,36 @@ export function findIncompleteLesson(progressOnItems, currentContentId, contentT
|
|
|
1306
1339
|
|
|
1307
1340
|
async function popPinnedItemFromContentsOrPlaylistMap(pinned, contentsMap, playlistItems) {
|
|
1308
1341
|
if (!pinned) return null
|
|
1309
|
-
const {id, progressType, pinnedAt} = pinned
|
|
1342
|
+
const { id, progressType, pinnedAt } = pinned
|
|
1310
1343
|
let item = null
|
|
1311
1344
|
if (progressType === 'content') {
|
|
1312
1345
|
const pinnedId = parseInt(id)
|
|
1313
1346
|
if (contentsMap.has(pinnedId)) {
|
|
1314
1347
|
item = contentsMap.get(pinnedId)
|
|
1315
1348
|
contentsMap.delete(pinnedId)
|
|
1316
|
-
|
|
1317
1349
|
} else {
|
|
1318
1350
|
// we use fetchByRailContentIds so that we don't have the _type restriction in the query
|
|
1319
1351
|
let data = await fetchByRailContentIds([id], 'progress-tracker')
|
|
1320
|
-
item = await addContextToContent(() => data[0] ?? null,
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
}
|
|
1328
|
-
)
|
|
1352
|
+
item = await addContextToContent(() => data[0] ?? null, {
|
|
1353
|
+
addNextLesson: true,
|
|
1354
|
+
addNavigateTo: true,
|
|
1355
|
+
addProgressStatus: true,
|
|
1356
|
+
addProgressPercentage: true,
|
|
1357
|
+
addProgressTimestamp: true,
|
|
1358
|
+
})
|
|
1329
1359
|
}
|
|
1330
1360
|
}
|
|
1331
1361
|
if (progressType === 'playlist') {
|
|
1332
|
-
const pinnedPlaylist = playlistItems.find(p => p.playlist.id === id)
|
|
1362
|
+
const pinnedPlaylist = playlistItems.find((p) => p.playlist.id === id)
|
|
1333
1363
|
if (pinnedPlaylist) {
|
|
1334
|
-
playlistItems = playlistItems.filter(p => p.playlist.id !== id)
|
|
1364
|
+
playlistItems = playlistItems.filter((p) => p.playlist.id !== id)
|
|
1335
1365
|
item = pinnedPlaylist
|
|
1336
1366
|
} else {
|
|
1337
1367
|
const playlist = await fetchPlaylist(id)
|
|
1338
1368
|
item = {
|
|
1339
|
-
id:
|
|
1340
|
-
playlist:
|
|
1341
|
-
type:
|
|
1369
|
+
id: id,
|
|
1370
|
+
playlist: playlist,
|
|
1371
|
+
type: 'playlist',
|
|
1342
1372
|
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1343
1373
|
}
|
|
1344
1374
|
}
|
|
@@ -1347,11 +1377,11 @@ async function popPinnedItemFromContentsOrPlaylistMap(pinned, contentsMap, playl
|
|
|
1347
1377
|
}
|
|
1348
1378
|
|
|
1349
1379
|
function popContentAndRemoveChildrenFromContentsMap(content, contentsMap) {
|
|
1350
|
-
const children = content.children.map(child => child.id)
|
|
1380
|
+
const children = content.children.map((child) => child.id)
|
|
1351
1381
|
if (contentsMap.has(content.id)) {
|
|
1352
1382
|
contentsMap.delete(content.id)
|
|
1353
1383
|
}
|
|
1354
|
-
children.forEach(child => {
|
|
1384
|
+
children.forEach((child) => {
|
|
1355
1385
|
if (contentsMap.has(child)) {
|
|
1356
1386
|
contentsMap.delete(child)
|
|
1357
1387
|
}
|