import React, { useEffect } from "react";
import { Searchbar, Title } from 'react-native-paper';
import { ScrollView, ActivityIndicator, StyleSheet, SafeAreaView, View } from 'react-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 [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(() => {
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;
}
}, [viewer])
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) => (
));
};
const renderWatchingCards = (items = []) => {
return items.map((item, index) => (
));
};
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 (
{groups.length ? <>> : }
{
(searchQuery.length === 0 && watching.length) ?
{i18n.t("message.continueWatching")}:
{renderWatchingCards(watching)}
: <>>
}
{
(searchQuery.length === 0 && groups.length) ?
<>
{i18n.t("message.recentlyAdded")}:
{renderCourseCards(groups)}
> : <>>
}
{
(searchQuery.length === 0 && popular.length) ?
<>
{i18n.t("message.popularCourses")}:
{renderCourseCards(popular)}
> : <>>
}
{
(searchQuery.length !== 0 && groups.length) ?
{renderCourseCards(groups, true)}
: <>>
}
)
}
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,
}
});