musora-content-services 2.28.6 → 2.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +38 -0
- package/link_mcs.sh +0 -0
- package/package.json +1 -1
- package/src/contentTypeConfig.js +69 -50
- package/src/services/config.js +0 -6
- package/src/services/content-org/playlists-types.js +10 -0
- package/src/services/content-org/playlists.js +14 -14
- package/src/services/content.js +8 -9
- package/src/services/contentAggregator.js +45 -7
- package/src/services/contentProgress.js +67 -0
- package/src/services/gamification/awards.js +52 -33
- package/src/services/gamification/types.js +5 -23
- package/src/services/railcontent.js +25 -36
- package/src/services/recommendations.js +20 -18
- package/src/services/sanity.js +27 -57
- package/src/services/types.js +0 -8
- package/src/services/userActivity.js +340 -419
- package/test/initializeTests.js +0 -4
|
@@ -9,26 +9,33 @@ import {
|
|
|
9
9
|
fetchUserPracticeNotes,
|
|
10
10
|
fetchHandler,
|
|
11
11
|
fetchRecentUserActivities,
|
|
12
|
-
fetchLastInteractedChild,
|
|
13
12
|
} from './railcontent'
|
|
14
13
|
import { DataContext, UserActivityVersionKey } from './dataContext.js'
|
|
15
14
|
import { fetchByRailContentIds, fetchShows } from './sanity'
|
|
16
|
-
import {fetchPlaylist, fetchUserPlaylists} from
|
|
17
|
-
import {pinnedGuidedCourses} from
|
|
18
|
-
import {
|
|
15
|
+
import { fetchPlaylist, fetchUserPlaylists } from './content-org/playlists'
|
|
16
|
+
import { pinnedGuidedCourses } from './content-org/guided-courses'
|
|
17
|
+
import {
|
|
18
|
+
getMonday,
|
|
19
|
+
getWeekNumber,
|
|
20
|
+
isSameDate,
|
|
21
|
+
isNextDay,
|
|
22
|
+
getTimeRemainingUntilLocal,
|
|
23
|
+
toDayjs,
|
|
24
|
+
} from './dateUtils.js'
|
|
19
25
|
import { globalConfig } from './config'
|
|
20
|
-
import {collectionLessonTypes, lessonTypesMapping, progressTypesMapping, recentTypes, showsLessonTypes, songs} from "../contentTypeConfig";
|
|
21
26
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
import {
|
|
27
|
+
collectionLessonTypes,
|
|
28
|
+
progressTypesMapping,
|
|
29
|
+
recentTypes,
|
|
30
|
+
showsLessonTypes,
|
|
31
|
+
songs,
|
|
32
|
+
} from '../contentTypeConfig'
|
|
33
|
+
import { getAllStartedOrCompleted, getProgressStateByIds } from './contentProgress'
|
|
34
|
+
import { TabResponseType } from '../contentMetaData'
|
|
29
35
|
import dayjs from 'dayjs'
|
|
30
36
|
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore'
|
|
31
37
|
import weekOfYear from 'dayjs/plugin/weekOfYear'
|
|
38
|
+
import {addContextToContent} from "./contentAggregator.js";
|
|
32
39
|
|
|
33
40
|
const DATA_KEY_PRACTICES = 'practices'
|
|
34
41
|
const DATA_KEY_LAST_UPDATED_TIME = 'u'
|
|
@@ -88,7 +95,7 @@ export let userActivityContext = new DataContext(UserActivityVersionKey, fetchUs
|
|
|
88
95
|
* .catch(error => console.error(error));
|
|
89
96
|
*/
|
|
90
97
|
export async function getUserWeeklyStats() {
|
|
91
|
-
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
98
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
92
99
|
let data = await userActivityContext.getData()
|
|
93
100
|
let practices = data?.[DATA_KEY_PRACTICES] ?? {}
|
|
94
101
|
let sortedPracticeDays = Object.keys(practices)
|
|
@@ -99,10 +106,19 @@ export async function getUserWeeklyStats() {
|
|
|
99
106
|
let dailyStats = []
|
|
100
107
|
for (let i = 0; i < 7; i++) {
|
|
101
108
|
const day = startOfWeek.add(i, 'day')
|
|
102
|
-
let hasPractice = sortedPracticeDays.some((practiceDate) =>
|
|
103
|
-
|
|
109
|
+
let hasPractice = sortedPracticeDays.some((practiceDate) =>
|
|
110
|
+
isSameDate(practiceDate, day.format('YYYY-MM-DD'))
|
|
111
|
+
)
|
|
112
|
+
let isActive = isSameDate(today.format(), day.format())
|
|
104
113
|
let type = hasPractice ? 'tracked' : isActive ? 'active' : 'none'
|
|
105
|
-
dailyStats.push({
|
|
114
|
+
dailyStats.push({
|
|
115
|
+
key: i,
|
|
116
|
+
label: DAYS[i],
|
|
117
|
+
isActive,
|
|
118
|
+
inStreak: hasPractice,
|
|
119
|
+
type,
|
|
120
|
+
day: day.format('YYYY-MM-DD'),
|
|
121
|
+
})
|
|
106
122
|
}
|
|
107
123
|
|
|
108
124
|
let { streakMessage } = getStreaksAndMessage(practices)
|
|
@@ -169,7 +185,7 @@ export async function getUserMonthlyStats(params = {}) {
|
|
|
169
185
|
}
|
|
170
186
|
}
|
|
171
187
|
|
|
172
|
-
|
|
188
|
+
// let endOfMonth = new Date(year, month + 1, 0)
|
|
173
189
|
let endOfGrid = endOfMonth.clone()
|
|
174
190
|
while (endOfGrid.day() !== 0) {
|
|
175
191
|
endOfGrid = endOfGrid.add(1, 'day')
|
|
@@ -221,7 +237,7 @@ export async function getUserMonthlyStats(params = {}) {
|
|
|
221
237
|
|
|
222
238
|
// Filter past practices only
|
|
223
239
|
let filteredPractices = Object.entries(practices)
|
|
224
|
-
.filter(([date]) => dayjs
|
|
240
|
+
.filter(([date]) => dayjs(date).isSameOrBefore(endOfMonth))
|
|
225
241
|
.reduce((acc, [date, val]) => {
|
|
226
242
|
acc[date] = val
|
|
227
243
|
return acc
|
|
@@ -275,6 +291,7 @@ export async function getUserMonthlyStats(params = {}) {
|
|
|
275
291
|
*/
|
|
276
292
|
export async function recordUserPractice(practiceDetails) {
|
|
277
293
|
practiceDetails.auto = 0
|
|
294
|
+
practiceDetails.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
278
295
|
if (practiceDetails.content_id) {
|
|
279
296
|
practiceDetails.auto = 1
|
|
280
297
|
}
|
|
@@ -390,13 +407,11 @@ export async function restoreUserPractice(id) {
|
|
|
390
407
|
const restoredPractice = response.data.find((p) => p.id === id)
|
|
391
408
|
if (restoredPractice) {
|
|
392
409
|
await userActivityContext.updateLocal(async function (localContext) {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (localContext.data[DATA_KEY_PRACTICES][date]) {
|
|
396
|
-
localContext.data[DATA_KEY_PRACTICES][date] = []
|
|
410
|
+
if (localContext.data[DATA_KEY_PRACTICES][restoredPractice.day]) {
|
|
411
|
+
localContext.data[DATA_KEY_PRACTICES][restoredPractice.day] = []
|
|
397
412
|
}
|
|
398
413
|
response.data.forEach((restoredPractice) => {
|
|
399
|
-
localContext.data[DATA_KEY_PRACTICES][
|
|
414
|
+
localContext.data[DATA_KEY_PRACTICES][restoredPractice.day].push({
|
|
400
415
|
id: restoredPractice.id,
|
|
401
416
|
duration_seconds: restoredPractice.duration_seconds,
|
|
402
417
|
})
|
|
@@ -645,7 +660,7 @@ function calculateStreaks(practices, includeStreakMessage = false) {
|
|
|
645
660
|
let lastActiveDay = null
|
|
646
661
|
let streakMessage = ''
|
|
647
662
|
|
|
648
|
-
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
663
|
+
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
649
664
|
let sortedPracticeDays = Object.keys(practices)
|
|
650
665
|
.map((dateStr) => {
|
|
651
666
|
const [year, month, day] = dateStr.split('-').map(Number)
|
|
@@ -828,8 +843,6 @@ async function formatPracticeMeta(practices) {
|
|
|
828
843
|
const contentIds = practices.map((p) => p.content_id).filter((id) => id !== null)
|
|
829
844
|
const contents = await fetchByRailContentIds(contentIds)
|
|
830
845
|
|
|
831
|
-
const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
832
|
-
|
|
833
846
|
return practices.map((practice) => {
|
|
834
847
|
const content = contents.find((c) => c.id === practice.content_id) || {}
|
|
835
848
|
|
|
@@ -847,14 +860,13 @@ async function formatPracticeMeta(practices) {
|
|
|
847
860
|
content_type: getFormattedType(content.type || '', content.brand),
|
|
848
861
|
content_id: practice.content_id || null,
|
|
849
862
|
content_brand: content.brand || null,
|
|
850
|
-
created_at:
|
|
863
|
+
created_at: dayjs(practice.created_at),
|
|
851
864
|
sanity_type: content.type || null,
|
|
852
865
|
content_slug: content.slug || null,
|
|
853
866
|
}
|
|
854
867
|
})
|
|
855
868
|
}
|
|
856
869
|
|
|
857
|
-
|
|
858
870
|
/**
|
|
859
871
|
* Records a new user activity in the system.
|
|
860
872
|
*
|
|
@@ -898,6 +910,86 @@ export async function deleteUserActivity(id) {
|
|
|
898
910
|
const url = `/api/user-management-system/v1/activities/${id}`
|
|
899
911
|
return await fetchHandler(url, 'DELETE')
|
|
900
912
|
}
|
|
913
|
+
|
|
914
|
+
async function extractPinnedItemsAndSortAllItems(userPinnedItem, contentsMap, eligiblePlaylistItems, pinnedGuidedCourse, limit) {
|
|
915
|
+
let pinnedItem = await popPinnedItemFromContentsOrPlaylistMap(
|
|
916
|
+
userPinnedItem,
|
|
917
|
+
contentsMap,
|
|
918
|
+
eligiblePlaylistItems,
|
|
919
|
+
)
|
|
920
|
+
|
|
921
|
+
const guidedCourseID = pinnedGuidedCourse?.content_id
|
|
922
|
+
let combined = [];
|
|
923
|
+
if (pinnedGuidedCourse) {
|
|
924
|
+
const guidedCourseContent = contentsMap.get(guidedCourseID) ?? await addContextToContent(fetchByRailContentId, guidedCourseID, 'guided-course',
|
|
925
|
+
{
|
|
926
|
+
addNextLesson: true,
|
|
927
|
+
addNavigateTo: true,
|
|
928
|
+
addProgressStatus: true,
|
|
929
|
+
addProgressPercentage: true,
|
|
930
|
+
addProgressTimestamp: true,
|
|
931
|
+
})
|
|
932
|
+
contentsMap = popContentAndRemoveChildrenFromContentsMap(guidedCourseContent, contentsMap)
|
|
933
|
+
guidedCourseContent.pinned = true
|
|
934
|
+
combined.push(guidedCourseContent)
|
|
935
|
+
}
|
|
936
|
+
if (pinnedItem) {
|
|
937
|
+
pinnedItem.pinned = true
|
|
938
|
+
combined.push(pinnedItem)
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
const progressList = Array.from(contentsMap.values())
|
|
942
|
+
combined = [...combined, ...progressList, ...eligiblePlaylistItems]
|
|
943
|
+
return mergeAndSortItems(combined, limit)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function generateContentsMap(contents, playlistsContents) {
|
|
947
|
+
const excludedTypes = new Set([
|
|
948
|
+
'pack-bundle',
|
|
949
|
+
'learning-path-course',
|
|
950
|
+
'learning-path-level',
|
|
951
|
+
'guided-course-part',
|
|
952
|
+
]);
|
|
953
|
+
const existingShows = new Set();
|
|
954
|
+
const contentsMap = new Map();
|
|
955
|
+
const childToParentMap = {};
|
|
956
|
+
contents.forEach(content => {
|
|
957
|
+
if (Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0) {
|
|
958
|
+
childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
const allRecentTypeSet = new Set(
|
|
963
|
+
Object.values(recentTypes).flat()
|
|
964
|
+
)
|
|
965
|
+
contents.forEach(content => {
|
|
966
|
+
const id = content.id
|
|
967
|
+
const type = content.type
|
|
968
|
+
if (excludedTypes.has(type) || (!allRecentTypeSet.has(type) && !showsLessonTypes.includes(type)) ) return;
|
|
969
|
+
if (!childToParentMap[id]) {
|
|
970
|
+
// Shows don't have a parent to link them, but need to be handled as if they're a set of children
|
|
971
|
+
if (!existingShows.has(type)) {
|
|
972
|
+
contentsMap.set(id, content)
|
|
973
|
+
}
|
|
974
|
+
if (showsLessonTypes.includes(type)) {
|
|
975
|
+
existingShows.add(type)
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
})
|
|
979
|
+
|
|
980
|
+
// TODO this doesn't work for guided courses as the GC card takes precedence over the playlist card
|
|
981
|
+
// https://musora.atlassian.net/browse/BEH-812
|
|
982
|
+
if (playlistsContents) {
|
|
983
|
+
for (const item of playlistsContents) {
|
|
984
|
+
const contentId = item.id
|
|
985
|
+
contentsMap.delete(contentId)
|
|
986
|
+
const parentIds = item.parent_content_data || [];
|
|
987
|
+
parentIds.forEach(id => contentsMap.delete(id));
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return contentsMap;
|
|
991
|
+
}
|
|
992
|
+
|
|
901
993
|
/**
|
|
902
994
|
* Fetches and combines recent user progress rows and playlists, excluding certain types and parents.
|
|
903
995
|
*
|
|
@@ -912,11 +1004,7 @@ export async function deleteUserActivity(id) {
|
|
|
912
1004
|
* .catch(error => console.error(error));
|
|
913
1005
|
*/
|
|
914
1006
|
export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
915
|
-
|
|
916
|
-
'pack-bundle',
|
|
917
|
-
'learning-path-course',
|
|
918
|
-
'learning-path-level'
|
|
919
|
-
]);
|
|
1007
|
+
|
|
920
1008
|
// TODO slice progress to a reasonable number, say 100
|
|
921
1009
|
const [recentPlaylists, progressContents, allPinnedGuidedCourse, userPinnedItem ] = await Promise.all([
|
|
922
1010
|
fetchUserPlaylists(brand, { sort: '-last_progress', limit: limit}),
|
|
@@ -924,321 +1012,187 @@ export async function getProgressRows({ brand = null, limit = 8 } = {}) {
|
|
|
924
1012
|
pinnedGuidedCourses(brand),
|
|
925
1013
|
getUserPinnedItem(brand),
|
|
926
1014
|
])
|
|
927
|
-
|
|
928
1015
|
let pinnedGuidedCourse = allPinnedGuidedCourse?.[0] ?? null
|
|
929
1016
|
|
|
930
1017
|
const playlists = recentPlaylists?.data || [];
|
|
931
1018
|
const eligiblePlaylistItems = await getEligiblePlaylistItems(playlists);
|
|
932
|
-
const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.last_engaged_on);
|
|
1019
|
+
const playlistEngagedOnContents = eligiblePlaylistItems.map(item => item.playlist.last_engaged_on);
|
|
933
1020
|
|
|
934
1021
|
const nonPlaylistContentIds = Object.keys(progressContents)
|
|
935
1022
|
if (pinnedGuidedCourse) {
|
|
936
|
-
nonPlaylistContentIds.push(pinnedGuidedCourse.content_id)
|
|
1023
|
+
nonPlaylistContentIds.push(pinnedGuidedCourse.content_id)
|
|
937
1024
|
}
|
|
938
1025
|
if (userPinnedItem?.progressType === 'content') {
|
|
939
1026
|
nonPlaylistContentIds.push(userPinnedItem.id)
|
|
940
1027
|
}
|
|
941
1028
|
const [ playlistsContents, contents ] = await Promise.all([
|
|
942
|
-
fetchByRailContentIds
|
|
943
|
-
|
|
1029
|
+
addContextToContent(fetchByRailContentIds, playlistEngagedOnContents, 'progress-tracker', {
|
|
1030
|
+
addNextLesson: true,
|
|
1031
|
+
addNavigateTo: true,
|
|
1032
|
+
addProgressStatus: true,
|
|
1033
|
+
addProgressPercentage: true,
|
|
1034
|
+
addProgressTimestamp: true,
|
|
1035
|
+
}),
|
|
1036
|
+
addContextToContent(fetchByRailContentIds, nonPlaylistContentIds, 'progress-tracker', brand, {
|
|
1037
|
+
addNextLesson: true,
|
|
1038
|
+
addNavigateTo: true,
|
|
1039
|
+
addProgressStatus: true,
|
|
1040
|
+
addProgressPercentage: true,
|
|
1041
|
+
addProgressTimestamp: true,
|
|
1042
|
+
})
|
|
944
1043
|
]);
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
const existingShows = new Set();
|
|
948
|
-
// TODO this doesn't work for guided courses as the GC card takes precedence over the playlist card
|
|
949
|
-
// https://musora.atlassian.net/browse/BEH-812
|
|
950
|
-
for (const item of playlistsContents) {
|
|
951
|
-
|
|
952
|
-
const contentId = item.id ?? item.railcontent_id;
|
|
953
|
-
delete progressContents[contentId]
|
|
954
|
-
const parentIds = item.parent_content_data || [];
|
|
955
|
-
parentIds.forEach(id => delete progressContents[id] );
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const contentsMap = {};
|
|
960
|
-
contents.forEach(content => {
|
|
961
|
-
contentsMap[content.railcontent_id] = content;
|
|
962
|
-
});
|
|
963
|
-
const childToParentMap = {};
|
|
964
|
-
Object.values(contentsMap).forEach(content => {
|
|
965
|
-
if (Array.isArray(content.parent_content_data) && content.parent_content_data.length > 0) {
|
|
966
|
-
childToParentMap[content.id] = content.parent_content_data[content.parent_content_data.length - 1];
|
|
967
|
-
}
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
const allRecentTypeSet = new Set(
|
|
971
|
-
Object.values(recentTypes).flat()
|
|
972
|
-
)
|
|
973
|
-
const progressMap = new Map();
|
|
974
|
-
for (const [idStr, progress] of Object.entries(progressContents)) {
|
|
975
|
-
const id = parseInt(idStr);
|
|
976
|
-
const content = contentsMap[id];
|
|
977
|
-
if (!content || excludedTypes.has(content.type) || !allRecentTypeSet.has(content.type) ) continue;
|
|
978
|
-
const parentId = childToParentMap[id];
|
|
979
|
-
// Handle children with parents
|
|
980
|
-
if (parentId) {
|
|
981
|
-
const parentContent = contentsMap[parentId];
|
|
982
|
-
if (!parentContent || excludedTypes.has(parentContent.type)) continue;
|
|
983
|
-
const existing = progressMap.get(parentId);
|
|
984
|
-
if (existing) {
|
|
985
|
-
// If childIndex isn't already set, set it
|
|
986
|
-
if (existing.childIndex === undefined) {
|
|
987
|
-
existing.childIndex = id;
|
|
988
|
-
}
|
|
989
|
-
} else {
|
|
990
|
-
progressMap.set(parentId, {
|
|
991
|
-
id: parentId,
|
|
992
|
-
raw: parentContent,
|
|
993
|
-
state: progress.status,
|
|
994
|
-
percent: progress.progress,
|
|
995
|
-
progressTimestamp: progress.last_update * 1000,
|
|
996
|
-
childIndex: id
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
continue;
|
|
1000
|
-
}
|
|
1001
|
-
// Handle standalone parents
|
|
1002
|
-
if (!progressMap.has(id)) {
|
|
1003
|
-
if(!existingShows.has(content.type)){
|
|
1004
|
-
progressMap.set(id, {
|
|
1005
|
-
id,
|
|
1006
|
-
raw: content,
|
|
1007
|
-
state: progress.status,
|
|
1008
|
-
percent: progress.progress,
|
|
1009
|
-
progressTimestamp: progress.last_update * 1000
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
if(showsLessonTypes.includes(content.type)) {
|
|
1013
|
-
existingShows.add(content.type)
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
const pinnedItem = userPinnedItem ? await extractPinnedItem(
|
|
1018
|
-
userPinnedItem,
|
|
1019
|
-
progressMap,
|
|
1020
|
-
eligiblePlaylistItems,
|
|
1021
|
-
) : null
|
|
1022
|
-
|
|
1023
|
-
const pinnedId = pinnedItem?.id
|
|
1024
|
-
const guidedCourseID = pinnedGuidedCourse?.content_id
|
|
1025
|
-
let combined = [];
|
|
1026
|
-
if (pinnedGuidedCourse) {
|
|
1027
|
-
const guidedCourseContent = contentsMap[guidedCourseID]
|
|
1028
|
-
if (guidedCourseContent) {
|
|
1029
|
-
const temp = await extractPinnedGuidedCourseItem(guidedCourseContent, progressMap)
|
|
1030
|
-
temp.pinned = true
|
|
1031
|
-
combined.push(temp)
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
if (pinnedItem) {
|
|
1035
|
-
pinnedItem.pinned = true
|
|
1036
|
-
combined.push(pinnedItem)
|
|
1037
|
-
}
|
|
1038
|
-
const progressList = Array.from(progressMap.values())
|
|
1039
|
-
|
|
1040
|
-
const filteredProgressList = pinnedId
|
|
1041
|
-
? progressList.filter(item => !(item.id === pinnedId || item.id === guidedCourseID))
|
|
1042
|
-
: progressList;
|
|
1043
|
-
const filteredPlaylists = pinnedId
|
|
1044
|
-
? (eligiblePlaylistItems.filter(item => !(item.id === pinnedId || item.id === guidedCourseID)))
|
|
1045
|
-
: eligiblePlaylistItems;
|
|
1046
|
-
|
|
1047
|
-
combined = [...combined, ...filteredProgressList, ...filteredPlaylists]
|
|
1048
|
-
const finalCombined = mergeAndSortItems(combined, limit)
|
|
1044
|
+
const contentsMap = generateContentsMap(contents, playlistsContents);
|
|
1045
|
+
let combined = await extractPinnedItemsAndSortAllItems(userPinnedItem, contentsMap, eligiblePlaylistItems, pinnedGuidedCourse, limit);
|
|
1049
1046
|
const results = await Promise.all(
|
|
1050
|
-
|
|
1047
|
+
combined.slice(0, limit).map(item =>
|
|
1051
1048
|
item.type === 'playlist'
|
|
1052
1049
|
? processPlaylistItem(item)
|
|
1053
1050
|
: processContentItem(item)
|
|
1054
1051
|
)
|
|
1055
1052
|
);
|
|
1056
|
-
|
|
1053
|
+
console.log('HomePageProgressRows results: remove before merge', results)
|
|
1057
1054
|
return {
|
|
1058
1055
|
type: TabResponseType.PROGRESS_ROWS,
|
|
1059
1056
|
displayBrowseAll: combined.length > limit,
|
|
1060
|
-
data: results
|
|
1061
|
-
}
|
|
1057
|
+
data: results,
|
|
1058
|
+
}
|
|
1062
1059
|
}
|
|
1063
1060
|
|
|
1064
1061
|
async function getUserPinnedItem(brand) {
|
|
1065
|
-
const userRaw = await globalConfig.localStorage.getItem('user')
|
|
1066
|
-
const user = userRaw ? JSON.parse(userRaw) : {}
|
|
1062
|
+
const userRaw = await globalConfig.localStorage.getItem('user')
|
|
1063
|
+
const user = userRaw ? JSON.parse(userRaw) : {}
|
|
1067
1064
|
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
1068
1065
|
return user.brand_pinned_progress[brand] ?? null
|
|
1069
1066
|
}
|
|
1070
1067
|
|
|
1071
|
-
async function processContentItem(
|
|
1072
|
-
|
|
1073
|
-
const
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
if (
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
data.completed_children = completedCount;
|
|
1089
|
-
|
|
1090
|
-
if (item.childIndex) {
|
|
1091
|
-
let nextId = item.childIndex
|
|
1092
|
-
const nextByProgress = findIncompleteLesson(progressOnItems, item.childIndex, item.raw.type)
|
|
1093
|
-
nextId = nextByProgress ? nextByProgress : nextId
|
|
1094
|
-
|
|
1095
|
-
const nestedLessons = data.lessons
|
|
1096
|
-
.filter(item => Array.isArray(item.lessons))
|
|
1097
|
-
.flatMap(parent =>
|
|
1098
|
-
parent.lessons.map(lesson => ({
|
|
1099
|
-
...lesson,
|
|
1100
|
-
parent: {
|
|
1101
|
-
id: parent.id,
|
|
1102
|
-
slug: parent.slug,
|
|
1103
|
-
title: parent.title,
|
|
1104
|
-
type: parent.type
|
|
1105
|
-
}
|
|
1106
|
-
}))
|
|
1107
|
-
);
|
|
1108
|
-
|
|
1109
|
-
const lessons = (nestedLessons.length === 0) ? data.lessons : nestedLessons
|
|
1110
|
-
const nextLesson = lessons.find(lesson => lesson.id === nextId)
|
|
1111
|
-
data.first_incomplete_child = nextLesson?.parent ?? nextLesson
|
|
1112
|
-
data.second_incomplete_child = (nextLesson?.parent) ? nextLesson : null
|
|
1113
|
-
if(data.type === 'guided-course'){
|
|
1114
|
-
let isLocked = new Date(nextLesson.published_on) > new Date()
|
|
1115
|
-
data.thumbnail = nextLesson.thumbnail
|
|
1116
|
-
// USHP-4 completed
|
|
1117
|
-
if (status === 'completed') {
|
|
1118
|
-
// duplicated code to above, but here for clarity
|
|
1119
|
-
ctaText = 'Revisit Lessons'
|
|
1120
|
-
// USHP-1 if lesson locked show unlock in X time
|
|
1121
|
-
} else if (isLocked) {
|
|
1122
|
-
data.is_locked = true
|
|
1123
|
-
const timeRemaining = getTimeRemainingUntilLocal(nextLesson.published_on, {withTotalSeconds: true})
|
|
1124
|
-
data.time_remaining_seconds = timeRemaining.totalSeconds
|
|
1125
|
-
ctaText = 'Next lesson in ' + timeRemaining.formatted
|
|
1126
|
-
}
|
|
1127
|
-
// USHP-2 start course if not started
|
|
1128
|
-
else if (status === 'not-started') {
|
|
1129
|
-
ctaText = "Start Course"
|
|
1130
|
-
}
|
|
1131
|
-
// USHP-3 in progress for lesson
|
|
1132
|
-
else {
|
|
1133
|
-
ctaText = "Continue"
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1068
|
+
async function processContentItem(content) {
|
|
1069
|
+
const contentType = getFormattedType(content.type, content.brand);
|
|
1070
|
+
const isLive = content.isLive ?? false
|
|
1071
|
+
let ctaText = getDefaultCTATextForContent(content, contentType)
|
|
1072
|
+
|
|
1073
|
+
content.completed_children = await getCompletedChildren(content, contentType)
|
|
1074
|
+
|
|
1075
|
+
if (content.type === 'guided-course') {
|
|
1076
|
+
const nextLessonPublishedOn = content.children.find(child => child.id === content.navigateTo.id)?.published_on
|
|
1077
|
+
let isLocked = new Date(nextLessonPublishedOn) > new Date()
|
|
1078
|
+
if (isLocked) {
|
|
1079
|
+
content.is_locked = true
|
|
1080
|
+
const timeRemaining = getTimeRemainingUntilLocal(nextLessonPublishedOn, {withTotalSeconds: true})
|
|
1081
|
+
content.time_remaining_seconds = timeRemaining.totalSeconds
|
|
1082
|
+
ctaText = 'Next lesson in ' + timeRemaining.formatted
|
|
1083
|
+
} else if (!content.progressStatus || content.progressStatus === 'not-started' ) {
|
|
1084
|
+
ctaText = "Start Course"
|
|
1136
1085
|
}
|
|
1137
1086
|
}
|
|
1138
1087
|
|
|
1139
|
-
if(contentType
|
|
1140
|
-
const shows = await fetchShows(
|
|
1088
|
+
if (contentType === 'show'){
|
|
1089
|
+
const shows = await fetchShows(content.brand, content.type)
|
|
1141
1090
|
const showIds = shows.map(item => item.id);
|
|
1142
1091
|
const progressOnItems = await getProgressStateByIds(showIds);
|
|
1143
|
-
const
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1092
|
+
const completedShows = content.completed_children
|
|
1093
|
+
const progressTimestamp = content.progressTimestamp
|
|
1094
|
+
const wasPinned = content.pinned ?? false
|
|
1095
|
+
if (content.progressStatus === 'completed') {
|
|
1096
|
+
// this could be handled more gracefully if their was a parent content type for shows
|
|
1097
|
+
const nextByProgress = findIncompleteLesson(progressOnItems, content.id, content.type);
|
|
1098
|
+
content = shows.find(lesson => lesson.id === nextByProgress);
|
|
1099
|
+
content.completed_children = completedShows
|
|
1100
|
+
content.progressTimestamp = progressTimestamp
|
|
1101
|
+
content.pinned = wasPinned
|
|
1147
1102
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
ctaText = 'Revisit Lessons';
|
|
1103
|
+
content.child_count = shows.length;
|
|
1104
|
+
content.progressPercentage = Math.round((completedShows / shows.length) * 100);
|
|
1105
|
+
if (completedShows === shows.length) {
|
|
1106
|
+
ctaText = 'Revisit Show';
|
|
1153
1107
|
}
|
|
1154
1108
|
}
|
|
1155
1109
|
|
|
1156
1110
|
return {
|
|
1157
|
-
id:
|
|
1111
|
+
id: content.id,
|
|
1158
1112
|
progressType: 'content',
|
|
1159
1113
|
header: contentType,
|
|
1160
|
-
pinned:
|
|
1114
|
+
pinned: content.pinned ?? false,
|
|
1115
|
+
content: content,
|
|
1161
1116
|
body: {
|
|
1162
|
-
progressPercent: isLive ? undefined:
|
|
1163
|
-
thumbnail:
|
|
1164
|
-
title:
|
|
1117
|
+
progressPercent: isLive ? undefined: content.progressPercentage,
|
|
1118
|
+
thumbnail: content.thumbnail,
|
|
1119
|
+
title: content.title,
|
|
1165
1120
|
isLive: isLive,
|
|
1166
|
-
badge:
|
|
1167
|
-
isLocked:
|
|
1168
|
-
subtitle: !
|
|
1169
|
-
? (contentType === 'lesson' && isLive === false) ? `${
|
|
1170
|
-
: `${
|
|
1121
|
+
badge: content.badge ?? null,
|
|
1122
|
+
isLocked: content.is_locked ?? false,
|
|
1123
|
+
subtitle: !content.child_count || content.lesson_count === 1
|
|
1124
|
+
? (contentType === 'lesson' && isLive === false) ? `${content.progressPercentage}% Complete`: `${content.difficulty_string} • ${content.artist_name}`
|
|
1125
|
+
: `${content.completed_children} of ${content.lesson_count ?? content.child_count} Lessons Complete`
|
|
1171
1126
|
},
|
|
1172
1127
|
cta: {
|
|
1173
1128
|
text: ctaText,
|
|
1174
|
-
timeRemainingToUnlockSeconds:
|
|
1129
|
+
timeRemainingToUnlockSeconds: content.time_remaining_seconds ?? null,
|
|
1175
1130
|
action: {
|
|
1176
|
-
type:
|
|
1177
|
-
brand:
|
|
1178
|
-
id:
|
|
1179
|
-
slug:
|
|
1180
|
-
child:
|
|
1181
|
-
? {
|
|
1182
|
-
id: data.first_incomplete_child.id,
|
|
1183
|
-
type: data.first_incomplete_child.type,
|
|
1184
|
-
brand: data.first_incomplete_child.brand,
|
|
1185
|
-
slug: data.first_incomplete_child.slug,
|
|
1186
|
-
child: data.second_incomplete_child
|
|
1187
|
-
? {
|
|
1188
|
-
id: data.second_incomplete_child.id,
|
|
1189
|
-
type: data.second_incomplete_child.type,
|
|
1190
|
-
brand: data.second_incomplete_child.brand,
|
|
1191
|
-
slug: data.second_incomplete_child.slug
|
|
1192
|
-
}
|
|
1193
|
-
: null
|
|
1194
|
-
}
|
|
1195
|
-
: null
|
|
1131
|
+
type: content.type,
|
|
1132
|
+
brand: content.brand,
|
|
1133
|
+
id: content.id,
|
|
1134
|
+
slug: content.slug,
|
|
1135
|
+
child: content.navigateTo,
|
|
1196
1136
|
}
|
|
1197
1137
|
},
|
|
1198
|
-
|
|
1138
|
+
// *1000 is to match playlists which are saved in millisecond accuracy
|
|
1139
|
+
progressTimestamp: content.progressTimestamp * 1000
|
|
1199
1140
|
};
|
|
1200
1141
|
}
|
|
1201
1142
|
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
if (!allItemsCompleted) {
|
|
1212
|
-
const lastItemProgress = progressOnItems[playlist.last_engaged_on];
|
|
1213
|
-
const index = playlist.items.findIndex(i => i.content_id === playlist.last_engaged_on);
|
|
1214
|
-
if (lastItemProgress === 'completed') {
|
|
1215
|
-
nextItem = playlist.items[index + 1] ?? nextItem;
|
|
1216
|
-
} else {
|
|
1217
|
-
nextItem = playlist.items[index] ?? nextItem;
|
|
1218
|
-
}
|
|
1143
|
+
function getDefaultCTATextForContent(content, contentType)
|
|
1144
|
+
{
|
|
1145
|
+
let ctaText = 'Continue';
|
|
1146
|
+
if (content.progressStatus === 'completed')
|
|
1147
|
+
{
|
|
1148
|
+
if (contentType === songs[content.brand] || contentType === 'play along' || contentType === 'jam track') ctaText = 'Replay Song';
|
|
1149
|
+
if (contentType === 'lesson') ctaText = 'Revisit Lesson';
|
|
1150
|
+
if (contentType === 'song tutorial' || collectionLessonTypes.includes(contentType)) ctaText = 'Revisit Lessons' ;
|
|
1151
|
+
if (contentType === 'pack') ctaText = 'View Lessons'
|
|
1219
1152
|
}
|
|
1153
|
+
return ctaText
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
async function getCompletedChildren(content, contentType)
|
|
1157
|
+
{
|
|
1158
|
+
let completedChildren = null
|
|
1159
|
+
if (contentType === 'show') {
|
|
1160
|
+
const shows = await addContextToContent(fetchShows, content.brand, content.type, {
|
|
1161
|
+
addProgressStatus: true,
|
|
1162
|
+
})
|
|
1163
|
+
completedChildren = Object.values(shows).filter(show => show.progressStatus === 'completed').length;
|
|
1164
|
+
} else if (content.lesson_count > 0) {
|
|
1165
|
+
const lessonIds = getLeafNodes(content);
|
|
1166
|
+
const progressOnItems = await getProgressStateByIds(lessonIds);
|
|
1167
|
+
completedChildren = Object.values(progressOnItems).filter(value => value === 'completed').length;
|
|
1168
|
+
}
|
|
1169
|
+
return completedChildren
|
|
1170
|
+
}
|
|
1220
1171
|
|
|
1172
|
+
async function processPlaylistItem(item) {
|
|
1173
|
+
const playlist = item.playlist;
|
|
1221
1174
|
return {
|
|
1222
1175
|
id: playlist.id,
|
|
1223
1176
|
progressType: 'playlist',
|
|
1224
1177
|
header: 'playlist',
|
|
1225
1178
|
pinned: item.pinned ?? false,
|
|
1179
|
+
playlist: playlist,
|
|
1226
1180
|
body: {
|
|
1227
1181
|
first_items_thumbnail_url: playlist.first_items_thumbnail_url,
|
|
1228
|
-
title:
|
|
1229
|
-
subtitle:
|
|
1230
|
-
total_items:
|
|
1182
|
+
title: playlist.name,
|
|
1183
|
+
subtitle: `${playlist.duration_formated} • ${playlist.total_items} items • ${playlist.likes} likes • ${playlist.user.display_name}`,
|
|
1184
|
+
total_items: playlist.total_items,
|
|
1231
1185
|
},
|
|
1232
1186
|
progressTimestamp: item.progressTimestamp,
|
|
1233
|
-
cta:
|
|
1234
|
-
text:
|
|
1187
|
+
cta: {
|
|
1188
|
+
text: 'Continue',
|
|
1235
1189
|
action: {
|
|
1236
1190
|
brand: playlist.brand,
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
lastEngagedOn: playlist.last_engaged_on,
|
|
1240
|
-
lastEngagedOnItem: playlist.last_engaged_on_item,
|
|
1191
|
+
item_id: playlist.navigateTo.id ?? null,
|
|
1192
|
+
content_id: playlist.navigateTo.content_id ?? null,
|
|
1241
1193
|
type: 'playlists',
|
|
1194
|
+
// TODO depreciated, maintained to avoid breaking changes
|
|
1195
|
+
id: playlist.id,
|
|
1242
1196
|
}
|
|
1243
1197
|
}
|
|
1244
1198
|
}
|
|
@@ -1247,90 +1201,144 @@ async function processPlaylistItem(item) {
|
|
|
1247
1201
|
const getFormattedType = (type, brand) => {
|
|
1248
1202
|
for (const [key, values] of Object.entries(progressTypesMapping)) {
|
|
1249
1203
|
if (values.includes(type)) {
|
|
1250
|
-
return key === 'songs' ? songs[brand] : key
|
|
1204
|
+
return key === 'songs' ? songs[brand] : key
|
|
1251
1205
|
}
|
|
1252
1206
|
}
|
|
1253
1207
|
|
|
1254
|
-
return null
|
|
1255
|
-
}
|
|
1208
|
+
return null
|
|
1209
|
+
}
|
|
1256
1210
|
|
|
1257
|
-
function
|
|
1211
|
+
function getLeafNodes(content) {
|
|
1258
1212
|
const ids = [];
|
|
1259
|
-
function traverse(
|
|
1260
|
-
for (const item of
|
|
1261
|
-
if (item.
|
|
1262
|
-
traverse(item.
|
|
1263
|
-
}else if (item.id) {
|
|
1213
|
+
function traverse(children) {
|
|
1214
|
+
for (const item of children) {
|
|
1215
|
+
if (item.children) {
|
|
1216
|
+
traverse(item.children); // Recursively handle nested lessons
|
|
1217
|
+
} else if (item.id) {
|
|
1264
1218
|
ids.push(item.id);
|
|
1265
1219
|
}
|
|
1266
1220
|
}
|
|
1267
1221
|
}
|
|
1268
|
-
if (
|
|
1269
|
-
traverse(
|
|
1222
|
+
if (content && Array.isArray(content.children)) {
|
|
1223
|
+
traverse(content.children);
|
|
1270
1224
|
}
|
|
1271
|
-
|
|
1272
|
-
return ids;
|
|
1225
|
+
return ids
|
|
1273
1226
|
}
|
|
1274
1227
|
|
|
1275
|
-
|
|
1276
1228
|
async function getEligiblePlaylistItems(playlists) {
|
|
1277
|
-
const eligible = playlists.filter(p => p.last_progress && p.last_engaged_on)
|
|
1229
|
+
const eligible = playlists.filter((p) => p.last_progress && p.last_engaged_on)
|
|
1278
1230
|
return Promise.all(
|
|
1279
|
-
eligible.map(async p => {
|
|
1280
|
-
const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z')
|
|
1281
|
-
const timestamp = utcDate.getTime()
|
|
1231
|
+
eligible.map(async (p) => {
|
|
1232
|
+
const utcDate = new Date(p.last_progress.replace(' ', 'T') + 'Z')
|
|
1233
|
+
const timestamp = utcDate.getTime()
|
|
1282
1234
|
return {
|
|
1283
1235
|
type: 'playlist',
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1236
|
+
// Content timestamps are millisecond accurate so for comparison we bring this to the same resolution
|
|
1237
|
+
progressTimestamp: timestamp / 1000,
|
|
1238
|
+
playlist: p,
|
|
1239
|
+
id: p.id,
|
|
1287
1240
|
};
|
|
1288
1241
|
})
|
|
1289
|
-
)
|
|
1242
|
+
)
|
|
1290
1243
|
}
|
|
1291
1244
|
|
|
1292
1245
|
function mergeAndSortItems(items, limit) {
|
|
1293
|
-
const seen = new Set()
|
|
1294
|
-
const deduped = []
|
|
1246
|
+
const seen = new Set()
|
|
1247
|
+
const deduped = []
|
|
1295
1248
|
|
|
1296
1249
|
for (const item of items) {
|
|
1297
|
-
const key = `${item.id}-${item.type
|
|
1250
|
+
const key = `${item.id}-${item.type}`;
|
|
1298
1251
|
if (!seen.has(key)) {
|
|
1299
|
-
seen.add(key)
|
|
1300
|
-
deduped.push(item)
|
|
1252
|
+
seen.add(key)
|
|
1253
|
+
deduped.push(item)
|
|
1301
1254
|
}
|
|
1302
1255
|
}
|
|
1303
1256
|
|
|
1304
1257
|
return deduped
|
|
1305
|
-
.filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp
|
|
1258
|
+
.filter(item => typeof item.progressTimestamp === 'number' && item.progressTimestamp >= 0)
|
|
1306
1259
|
.sort((a, b) => {
|
|
1307
1260
|
if (a.pinned && !b.pinned) return -1;
|
|
1308
1261
|
if (!a.pinned && b.pinned) return 1;
|
|
1309
|
-
// TODO guided course should always be before user pinned item
|
|
1262
|
+
// TODO pinned guided course should always be before user pinned item
|
|
1310
1263
|
return b.progressTimestamp - a.progressTimestamp;
|
|
1311
1264
|
})
|
|
1312
|
-
.slice(0, limit + 5)
|
|
1265
|
+
.slice(0, limit + 5)
|
|
1313
1266
|
}
|
|
1314
1267
|
|
|
1315
1268
|
export function findIncompleteLesson(progressOnItems, currentContentId, contentType) {
|
|
1316
|
-
const ids = Object.keys(progressOnItems).map(Number)
|
|
1269
|
+
const ids = Object.keys(progressOnItems).map(Number)
|
|
1317
1270
|
if (contentType === 'guided-course') {
|
|
1318
1271
|
// Return first incomplete lesson
|
|
1319
|
-
return ids.find(id => progressOnItems[id] !== 'completed') || ids.at(0)
|
|
1272
|
+
return ids.find((id) => progressOnItems[id] !== 'completed') || ids.at(0)
|
|
1320
1273
|
}
|
|
1321
1274
|
|
|
1322
1275
|
// For other types, find next incomplete after current
|
|
1323
|
-
const currentIndex = ids.indexOf(Number(currentContentId))
|
|
1324
|
-
if (currentIndex === -1) return null
|
|
1276
|
+
const currentIndex = ids.indexOf(Number(currentContentId))
|
|
1277
|
+
if (currentIndex === -1) return null
|
|
1325
1278
|
|
|
1326
1279
|
for (let i = currentIndex + 1; i < ids.length; i++) {
|
|
1327
|
-
const id = ids[i]
|
|
1280
|
+
const id = ids[i]
|
|
1328
1281
|
if (progressOnItems[id] !== 'completed') {
|
|
1329
|
-
return id
|
|
1282
|
+
return id
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
return ids[0]
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async function popPinnedItemFromContentsOrPlaylistMap(pinned, contentsMap, playlistItems) {
|
|
1290
|
+
if (!pinned) return null
|
|
1291
|
+
const {id, progressType, pinnedAt} = pinned
|
|
1292
|
+
let item = null
|
|
1293
|
+
if (progressType === 'content') {
|
|
1294
|
+
const pinnedId = parseInt(id)
|
|
1295
|
+
if (contentsMap.has(pinnedId)) {
|
|
1296
|
+
item = contentsMap.get(pinnedId)
|
|
1297
|
+
contentsMap.delete(pinnedId)
|
|
1298
|
+
|
|
1299
|
+
} else {
|
|
1300
|
+
// we use fetchByRailContentIds so that we don't have the _type restriction in the query
|
|
1301
|
+
let data = await fetchByRailContentIds([id], 'progress-tracker')
|
|
1302
|
+
item = await addContextToContent(() => data[0] ?? null,
|
|
1303
|
+
{
|
|
1304
|
+
addNextLesson: true,
|
|
1305
|
+
addNavigateTo: true,
|
|
1306
|
+
addProgressStatus: true,
|
|
1307
|
+
addProgressPercentage: true,
|
|
1308
|
+
addProgressTimestamp: true
|
|
1309
|
+
}
|
|
1310
|
+
)
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
if (progressType === 'playlist') {
|
|
1314
|
+
const pinnedPlaylist = playlistItems.find(p => p.playlist.id === id)
|
|
1315
|
+
if (pinnedPlaylist) {
|
|
1316
|
+
playlistItems = playlistItems.filter(p => p.playlist.id !== id)
|
|
1317
|
+
item = pinnedPlaylist
|
|
1318
|
+
} else {
|
|
1319
|
+
const playlist = await fetchPlaylist(id)
|
|
1320
|
+
item = {
|
|
1321
|
+
id: id,
|
|
1322
|
+
playlist: playlist,
|
|
1323
|
+
type: 'playlist',
|
|
1324
|
+
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1325
|
+
}
|
|
1330
1326
|
}
|
|
1331
1327
|
}
|
|
1328
|
+
return item
|
|
1329
|
+
}
|
|
1332
1330
|
|
|
1333
|
-
|
|
1331
|
+
function popContentAndRemoveChildrenFromContentsMap(content, contentsMap) {
|
|
1332
|
+
const children = content.children.map(child => child.id)
|
|
1333
|
+
if (contentsMap.has(content.id)) {
|
|
1334
|
+
contentsMap.delete(content.id)
|
|
1335
|
+
}
|
|
1336
|
+
children.forEach(child => {
|
|
1337
|
+
if (contentsMap.has(child)) {
|
|
1338
|
+
contentsMap.delete(child)
|
|
1339
|
+
}
|
|
1340
|
+
})
|
|
1341
|
+
return contentsMap
|
|
1334
1342
|
}
|
|
1335
1343
|
|
|
1336
1344
|
/**
|
|
@@ -1347,16 +1355,16 @@ export function findIncompleteLesson(progressOnItems, currentContentId, contentT
|
|
|
1347
1355
|
* .catch(error => console.error(error));
|
|
1348
1356
|
*/
|
|
1349
1357
|
export async function pinProgressRow(brand, id, progressType) {
|
|
1350
|
-
const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}
|
|
1358
|
+
const url = `/api/user-management-system/v1/progress/pin?brand=${brand}&id=${id}&progressType=${progressType}`
|
|
1351
1359
|
const response = await fetchHandler(url, 'PUT', null)
|
|
1352
1360
|
if (response && !response.error && response['action'] === 'update_user_pin') {
|
|
1353
1361
|
await updateUserPinnedProgressRow(brand, {
|
|
1354
1362
|
id,
|
|
1355
1363
|
progressType,
|
|
1356
1364
|
pinnedAt: new Date().toISOString(),
|
|
1357
|
-
})
|
|
1365
|
+
})
|
|
1358
1366
|
}
|
|
1359
|
-
return response
|
|
1367
|
+
return response
|
|
1360
1368
|
}
|
|
1361
1369
|
/**
|
|
1362
1370
|
* Unpins the current pinned progress row for a user, scoped by brand.
|
|
@@ -1381,113 +1389,26 @@ export async function unpinProgressRow(brand, id) {
|
|
|
1381
1389
|
}
|
|
1382
1390
|
|
|
1383
1391
|
async function updateUserPinnedProgressRow(brand, pinnedData) {
|
|
1384
|
-
const userRaw = await globalConfig.localStorage.getItem('user')
|
|
1385
|
-
const user = userRaw ? JSON.parse(userRaw) : {}
|
|
1392
|
+
const userRaw = await globalConfig.localStorage.getItem('user')
|
|
1393
|
+
const user = userRaw ? JSON.parse(userRaw) : {}
|
|
1386
1394
|
user.brand_pinned_progress = user.brand_pinned_progress || {}
|
|
1387
1395
|
user.brand_pinned_progress[brand] = pinnedData
|
|
1388
1396
|
await globalConfig.localStorage.setItem('user', JSON.stringify(user))
|
|
1389
1397
|
}
|
|
1390
1398
|
|
|
1391
|
-
async function extractPinnedItem(pinned, progressMap, playlistItems) {
|
|
1392
|
-
const {id, progressType, pinnedAt} = pinned
|
|
1393
|
-
|
|
1394
|
-
if (progressType === 'content') {
|
|
1395
|
-
const pinnedId = parseInt(id)
|
|
1396
|
-
if (progressMap.has(pinnedId)) {
|
|
1397
|
-
const item = progressMap.get(pinnedId)
|
|
1398
|
-
progressMap.delete(pinnedId)
|
|
1399
|
-
return item
|
|
1400
|
-
} else {
|
|
1401
|
-
const content = await fetchByRailContentIds([`${pinnedId}`], 'progress-tracker')
|
|
1402
|
-
const firstLessonId = getFirstLeafLessonId(content[0])
|
|
1403
|
-
return {
|
|
1404
|
-
id: pinnedId,
|
|
1405
|
-
state: 'started',
|
|
1406
|
-
percent: 0,
|
|
1407
|
-
raw: content[0],
|
|
1408
|
-
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1409
|
-
childIndex: firstLessonId
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
if (progressType === 'playlist') {
|
|
1414
|
-
const pinnedPlaylist = playlistItems.find(p => p.raw.id === id)
|
|
1415
|
-
if (pinnedPlaylist) {
|
|
1416
|
-
return pinnedPlaylist
|
|
1417
|
-
}else{
|
|
1418
|
-
const playlist = await fetchPlaylist(id)
|
|
1419
|
-
return {
|
|
1420
|
-
id: id,
|
|
1421
|
-
raw: playlist,
|
|
1422
|
-
progressTimestamp: new Date(pinnedAt).getTime(),
|
|
1423
|
-
type: 'playlist',
|
|
1424
|
-
last_engaged_on: playlist.last_engaged_on,
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
return null
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
async function extractPinnedGuidedCourseItem(guidedCourse, progressMap) {
|
|
1433
|
-
const children = guidedCourse.lessons.map(child => child.id)
|
|
1434
|
-
let existingGuidedCourseProgress = null
|
|
1435
|
-
if (progressMap.has(guidedCourse.id)) {
|
|
1436
|
-
existingGuidedCourseProgress = progressMap.get(guidedCourse.id)
|
|
1437
|
-
progressMap.delete(guidedCourse.id)
|
|
1438
|
-
}
|
|
1439
|
-
let lastChild = null
|
|
1440
|
-
children.forEach(child => {
|
|
1441
|
-
if (progressMap.has(child)) {
|
|
1442
|
-
let childProgress = progressMap.get(child)
|
|
1443
|
-
if (!lastChild && childProgress.state !== 'completed') {
|
|
1444
|
-
lastChild = childProgress
|
|
1445
|
-
lastChild.id = child
|
|
1446
|
-
}
|
|
1447
|
-
progressMap.delete(child)
|
|
1448
|
-
}
|
|
1449
|
-
})
|
|
1450
|
-
return existingGuidedCourseProgress ?? {
|
|
1451
|
-
id: guidedCourse.id,
|
|
1452
|
-
state: 'not-started',
|
|
1453
|
-
percent: 0,
|
|
1454
|
-
raw: guidedCourse,
|
|
1455
|
-
pinned: true,
|
|
1456
|
-
progressTimestamp: new Date().getTime(),
|
|
1457
|
-
childIndex: guidedCourse.id,
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
function getFirstLeafLessonId(data) {
|
|
1462
|
-
function findFirstLeaf(lessons) {
|
|
1463
|
-
for (const item of lessons) {
|
|
1464
|
-
if (!item.lessons || item.lessons.length === 0) {
|
|
1465
|
-
return item.id || null
|
|
1466
|
-
}
|
|
1467
|
-
const found = findFirstLeaf(item.lessons)
|
|
1468
|
-
if (found) return found
|
|
1469
|
-
}
|
|
1470
|
-
return null
|
|
1471
|
-
}
|
|
1472
|
-
|
|
1473
|
-
return data.lessons ? findFirstLeaf(data.lessons) : null
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
1399
|
export async function fetchRecentActivitiesActiveTabs() {
|
|
1477
1400
|
const url = `/api/user-management-system/v1/activities/tabs`
|
|
1478
1401
|
try {
|
|
1479
|
-
const tabs = await fetchHandler(url, 'GET')
|
|
1480
|
-
const activitiesTabs = []
|
|
1402
|
+
const tabs = await fetchHandler(url, 'GET')
|
|
1403
|
+
const activitiesTabs = []
|
|
1481
1404
|
|
|
1482
|
-
tabs.forEach(tab => {
|
|
1483
|
-
activitiesTabs.push({ name: tab.label, short_name:tab.label })
|
|
1484
|
-
})
|
|
1405
|
+
tabs.forEach((tab) => {
|
|
1406
|
+
activitiesTabs.push({ name: tab.label, short_name: tab.label })
|
|
1407
|
+
})
|
|
1485
1408
|
|
|
1486
|
-
return activitiesTabs
|
|
1409
|
+
return activitiesTabs
|
|
1487
1410
|
} catch (error) {
|
|
1488
|
-
console.error('Error fetching activity tabs:', error)
|
|
1489
|
-
return []
|
|
1411
|
+
console.error('Error fetching activity tabs:', error)
|
|
1412
|
+
return []
|
|
1490
1413
|
}
|
|
1491
1414
|
}
|
|
1492
|
-
|
|
1493
|
-
|