303 lines
10 KiB
JavaScript
303 lines
10 KiB
JavaScript
import React, { useEffect } from "react";
|
|
import { Searchbar, Title } from 'react-native-paper';
|
|
import { ScrollView, ActivityIndicator, StyleSheet, SafeAreaView, View } from 'react-native';
|
|
import { useIsFocused } from "@react-navigation/native";
|
|
import API from "../API";
|
|
import CourseCard from "../components/CourseCard";
|
|
import { useSnapshot } from 'valtio';
|
|
import GlobalState from '../contexts/GlobalState.js';
|
|
import i18n from "../i18nMessages.js";
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
const COURSES_LOG_PREFIX = '[Courses]';
|
|
const logCourses = (...args) => {
|
|
if (__DEV__) console.log(COURSES_LOG_PREFIX, ...args);
|
|
};
|
|
|
|
const emptyCoursesData = {
|
|
courses: [],
|
|
popular: [],
|
|
watching: [],
|
|
};
|
|
|
|
const getCourses = async (profileObj = {}) => {
|
|
let courses = [];
|
|
let popular = [];
|
|
logCourses('getCourses:start', {
|
|
viewerId: profileObj?._id || null,
|
|
hasViewerData: !!profileObj?.data,
|
|
});
|
|
await API.getCourses().then((data) => {
|
|
const groups = Array.isArray(data?.groups) ? data.groups : [];
|
|
courses = groups;
|
|
popular = [...groups].sort((a, b) => {
|
|
return Object.keys(b.subscribed).length - Object.keys(a.subscribed).length;
|
|
});
|
|
logCourses('getCourses:groups', {
|
|
groups: groups.length,
|
|
popular: popular.length,
|
|
});
|
|
});
|
|
let watching = {};
|
|
let watchingProms = [];
|
|
let watchingStats = {
|
|
rawEntries: 0,
|
|
explicitProfileId: 0,
|
|
fallbackByPost: 0,
|
|
skippedNoProgress: 0,
|
|
skippedNoProfileId: 0,
|
|
courseProfilesResolved: 0,
|
|
};
|
|
const pushWatchingProgress = (profile, progress, profileId) => {
|
|
if (!profile?.isCourse) return;
|
|
if (!watching[profileId]) {
|
|
watching[profileId] = { profile, progress: [], mostRecent: 0 };
|
|
}
|
|
if (watching[profileId].mostRecent < (progress?.ts || 0)) {
|
|
watching[profileId].mostRecent = progress.ts || 0;
|
|
}
|
|
watching[profileId].progress.push(progress);
|
|
watchingStats.courseProfilesResolved += 1;
|
|
};
|
|
let viewerData = {};
|
|
try {
|
|
// Detach potential proxy/non-extensible objects before iterating keys.
|
|
viewerData = JSON.parse(JSON.stringify(profileObj?.data || {}));
|
|
} catch (error) {
|
|
viewerData = {};
|
|
}
|
|
const viewerKeys = Object.keys(viewerData);
|
|
logCourses('watching:viewerData', {
|
|
entries: viewerKeys.length,
|
|
sampleKeys: viewerKeys.slice(0, 5),
|
|
});
|
|
Object.keys(viewerData).forEach((videoId) => {
|
|
watchingStats.rawEntries += 1;
|
|
const progress = viewerData[videoId];
|
|
if (!progress) {
|
|
watchingStats.skippedNoProgress += 1;
|
|
return;
|
|
}
|
|
|
|
const explicitProfileId = progress.profileId || progress.profileid;
|
|
if (explicitProfileId) {
|
|
watchingStats.explicitProfileId += 1;
|
|
watchingProms.push(API.getUserProfile(explicitProfileId).then((profile) => {
|
|
pushWatchingProgress(profile, progress, explicitProfileId);
|
|
}));
|
|
return;
|
|
}
|
|
|
|
// Backward compatibility: old progress entries may not include profileId.
|
|
const postId = progress.postId || videoId;
|
|
if (!postId) {
|
|
watchingStats.skippedNoProfileId += 1;
|
|
return;
|
|
}
|
|
watchingStats.fallbackByPost += 1;
|
|
watchingProms.push(
|
|
API.getPost(postId).then((post) => {
|
|
const fallbackProfileId = post?.profileid;
|
|
if (!fallbackProfileId) return 0;
|
|
return API.getUserProfile(fallbackProfileId).then((profile) => {
|
|
pushWatchingProgress(profile, progress, fallbackProfileId);
|
|
});
|
|
})
|
|
);
|
|
});
|
|
let watchingArray = [];
|
|
await Promise.all(watchingProms).then(() => {
|
|
for (const courseId in watching) {
|
|
watchingArray.push(watching[courseId]);
|
|
}
|
|
watchingArray = watchingArray.sort((a, b) => {
|
|
return b.mostRecent - a.mostRecent;
|
|
});
|
|
});
|
|
logCourses('watching:resolved', {
|
|
...watchingStats,
|
|
uniqueCourses: watchingArray.length,
|
|
});
|
|
return {
|
|
courses: courses.slice(0, 10),
|
|
popular: popular.slice(0, 10),
|
|
watching: watchingArray.slice(0, 10),
|
|
}
|
|
}
|
|
|
|
const storeCoursesCache = async (value) => {
|
|
try {
|
|
const jsonValue = JSON.stringify(value)
|
|
await AsyncStorage.setItem('courses', jsonValue)
|
|
} catch (e) {
|
|
}
|
|
}
|
|
|
|
const getCoursesCache = async () => {
|
|
try {
|
|
const value = await AsyncStorage.getItem('courses')
|
|
if (value !== null) {
|
|
return JSON.parse(value);
|
|
}
|
|
return emptyCoursesData;
|
|
} catch (e) {
|
|
return emptyCoursesData;
|
|
}
|
|
}
|
|
|
|
const Courses = () => {
|
|
const gState = useSnapshot(GlobalState);
|
|
const viewer = gState.me;
|
|
const isFocused = useIsFocused();
|
|
const [searchQuery, setSearchQuery] = React.useState('');
|
|
const [groups, setGroups] = React.useState([]);
|
|
const [popular, setPopular] = React.useState([]);
|
|
const [watching, setWatching] = React.useState([]);
|
|
const [queryTimer, setQueryTimer] = React.useState(0);
|
|
|
|
useEffect(() => {
|
|
if (!isFocused) return;
|
|
let subscribed = true;
|
|
const getData = async () => {
|
|
const cached = await getCoursesCache();
|
|
logCourses('cache:read', {
|
|
courses: Array.isArray(cached?.courses) ? cached.courses.length : 0,
|
|
popular: Array.isArray(cached?.popular) ? cached.popular.length : 0,
|
|
watching: Array.isArray(cached?.watching) ? cached.watching.length : 0,
|
|
});
|
|
if (subscribed) {
|
|
setGroups(Array.isArray(cached?.courses) ? cached.courses : []);
|
|
setPopular(Array.isArray(cached?.popular) ? cached.popular : []);
|
|
setWatching(Array.isArray(cached?.watching) ? cached.watching : []);
|
|
}
|
|
let r = await getCourses(viewer);
|
|
logCourses('live:resolved', {
|
|
courses: Array.isArray(r?.courses) ? r.courses.length : 0,
|
|
popular: Array.isArray(r?.popular) ? r.popular.length : 0,
|
|
watching: Array.isArray(r?.watching) ? r.watching.length : 0,
|
|
});
|
|
if(subscribed){
|
|
setGroups(r.courses || []);
|
|
setPopular(r.popular || []);
|
|
setWatching(r.watching || []);
|
|
}
|
|
storeCoursesCache(r);
|
|
};
|
|
getData();
|
|
return () => {
|
|
subscribed = false;
|
|
}
|
|
}, [isFocused, viewer?._id])
|
|
|
|
const onChangeSearch = query => {
|
|
setSearchQuery(query);
|
|
if (queryTimer) clearTimeout(queryTimer);
|
|
let timerId = setTimeout(() => {
|
|
if (!query) {
|
|
return API.getCourses('').then((data) => {
|
|
setGroups(data.groups || []);
|
|
});
|
|
}
|
|
API.searchCourses(query).then((data) => {
|
|
setGroups(data.groups || []);
|
|
})
|
|
|
|
}, 300);
|
|
setQueryTimer(timerId);
|
|
};
|
|
const renderCourseCards = (items = [], twoCols = false) => {
|
|
return items.map((item, index) => (
|
|
<CourseCard
|
|
key={item?._id || item?.profile?._id || `course-${index}`}
|
|
profileObj={item}
|
|
twoCols={twoCols}
|
|
/>
|
|
));
|
|
};
|
|
const renderWatchingCards = (items = []) => {
|
|
return items.map((item, index) => (
|
|
<CourseCard
|
|
key={item?.profile?._id || item?._id || `watching-${index}`}
|
|
profileObj={item?.profile}
|
|
/>
|
|
));
|
|
};
|
|
|
|
useEffect(() => {
|
|
logCourses('render', {
|
|
searchQueryLength: searchQuery.length,
|
|
groups: groups.length,
|
|
popular: popular.length,
|
|
watching: watching.length,
|
|
showWatching: searchQuery.length === 0 && watching.length > 0,
|
|
});
|
|
}, [searchQuery, groups.length, popular.length, watching.length]);
|
|
|
|
return (
|
|
<SafeAreaView style={styles.container}>
|
|
<Searchbar
|
|
placeholder = {i18n.t("message.searchCourses")}
|
|
onChangeText={onChangeSearch}
|
|
value={searchQuery}
|
|
/>
|
|
{groups.length ? <></> : <ActivityIndicator />}
|
|
<ScrollView>
|
|
{
|
|
(searchQuery.length === 0 && watching.length) ?
|
|
<View>
|
|
<Title style={styles.title} >{i18n.t("message.continueWatching")}:</Title>
|
|
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
|
|
{renderWatchingCards(watching)}
|
|
</ScrollView>
|
|
</View> : <></>
|
|
}
|
|
{
|
|
(searchQuery.length === 0 && groups.length) ?
|
|
<>
|
|
<Title style={styles.title} >{i18n.t("message.recentlyAdded")}:</Title>
|
|
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
|
|
{renderCourseCards(groups)}
|
|
</ScrollView>
|
|
</> : <></>
|
|
}
|
|
{
|
|
(searchQuery.length === 0 && popular.length) ?
|
|
<>
|
|
<Title style={styles.title} >{i18n.t("message.popularCourses")}:</Title>
|
|
<ScrollView horizontal={true} showsHorizontalScrollIndicator={false}>
|
|
{renderCourseCards(popular)}
|
|
</ScrollView>
|
|
</> : <></>
|
|
}
|
|
{
|
|
(searchQuery.length !== 0 && groups.length) ?
|
|
<View style={styles.searchGrid}>
|
|
{renderCourseCards(groups, true)}
|
|
</View> : <></>
|
|
}
|
|
</ScrollView>
|
|
</SafeAreaView>
|
|
)
|
|
}
|
|
|
|
export default Courses;
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1
|
|
},
|
|
title: {
|
|
padding: 10,
|
|
fontSize: 30,
|
|
marginTop: 15,
|
|
fontWeight: "bold",
|
|
color: "#777"
|
|
},
|
|
searchGrid: {
|
|
flexDirection: 'row',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-between',
|
|
paddingHorizontal: 4,
|
|
}
|
|
});
|