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, } });