From f526913f309e4f8c6ce1154b76172ea3b34f7493 Mon Sep 17 00:00:00 2001 From: austinkelsay Date: Sun, 11 May 2025 11:14:54 -0500 Subject: [PATCH] course decryption logic in its own hook --- src/hooks/encryption/useCourseDecryption.js | 151 ++++++++++++++++++++ src/pages/course/[slug]/index.js | 150 +------------------ 2 files changed, 153 insertions(+), 148 deletions(-) create mode 100644 src/hooks/encryption/useCourseDecryption.js diff --git a/src/hooks/encryption/useCourseDecryption.js b/src/hooks/encryption/useCourseDecryption.js new file mode 100644 index 0000000..6e9046f --- /dev/null +++ b/src/hooks/encryption/useCourseDecryption.js @@ -0,0 +1,151 @@ +import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; +import { useDecryptContent } from './useDecryptContent'; + +const useCourseDecryption = (session, paidCourse, course, lessons, setLessons, router) => { + const [decryptedLessonIds, setDecryptedLessonIds] = useState({}); + const [loading, setLoading] = useState(false); + const { decryptContent } = useDecryptContent(); + const processingRef = useRef(false); + const lastLessonIdRef = useRef(null); + const retryCountRef = useRef({}); + const MAX_RETRIES = 3; + + // Get the current active lesson + const currentLessonIndex = router.query.active ? parseInt(router.query.active, 10) : 0; + const currentLesson = lessons.length > 0 ? lessons[currentLessonIndex] : null; + const currentLessonId = currentLesson?.id; + + // Check if the current lesson has been decrypted + const isCurrentLessonDecrypted = + !paidCourse || + (currentLessonId && decryptedLessonIds[currentLessonId]); + + // Check user access + const hasAccess = useMemo(() => { + if (!session?.user || !paidCourse || !course) return false; + + return ( + session.user.purchased?.some(purchase => purchase.courseId === course?.d) || + session.user?.role?.subscribed || + session.user?.pubkey === course?.pubkey + ); + }, [session, paidCourse, course]); + + // Reset retry count when lesson changes + useEffect(() => { + if (currentLessonId && lastLessonIdRef.current !== currentLessonId) { + retryCountRef.current[currentLessonId] = 0; + } + }, [currentLessonId]); + + // Simplified decrypt function + const decryptCurrentLesson = useCallback(async () => { + if (!currentLesson || !hasAccess || !paidCourse) return; + if (processingRef.current) return; + if (decryptedLessonIds[currentLesson.id]) return; + if (!currentLesson.content) return; + + // Check retry count + if (!retryCountRef.current[currentLesson.id]) { + retryCountRef.current[currentLesson.id] = 0; + } + + // Limit maximum retries + if (retryCountRef.current[currentLesson.id] >= MAX_RETRIES) { + return; + } + + // Increment retry count + retryCountRef.current[currentLesson.id]++; + + try { + processingRef.current = true; + setLoading(true); + + // Start the decryption process + const decryptionPromise = decryptContent(currentLesson.content); + + // Add safety timeout to prevent infinite processing + let timeoutId; + const timeoutPromise = new Promise((_, reject) => { + timeoutId = setTimeout(() => { + // Cancel the in-flight request when timeout occurs + if (decryptionPromise.cancel) { + decryptionPromise.cancel(); + } + reject(new Error('Decryption timeout')); + }, 10000); + }); + + // Use a separate try-catch for the race + let decryptedContent; + try { + // Race between decryption and timeout + decryptedContent = await Promise.race([ + decryptionPromise, + timeoutPromise + ]); + + // Clear the timeout if decryption wins + clearTimeout(timeoutId); + } catch (error) { + // If timeout or network error, schedule a retry + setTimeout(() => { + processingRef.current = false; + decryptCurrentLesson(); + }, 5000); + throw error; + } + + if (!decryptedContent) { + return; + } + + // Update the lessons array with decrypted content + const updatedLessons = lessons.map(lesson => + lesson.id === currentLesson.id + ? { ...lesson, content: decryptedContent } + : lesson + ); + + setLessons(updatedLessons); + + // Mark this lesson as decrypted + setDecryptedLessonIds(prev => ({ + ...prev, + [currentLesson.id]: true + })); + + // Reset retry counter on success + retryCountRef.current[currentLesson.id] = 0; + } catch (error) { + // Silent error handling to prevent UI disruption + } finally { + setLoading(false); + processingRef.current = false; + } + }, [currentLesson, hasAccess, paidCourse, decryptContent, lessons, setLessons, decryptedLessonIds]); + + // Run decryption when lesson changes + useEffect(() => { + if (!currentLessonId) return; + + // Skip if the lesson hasn't changed, unless it failed decryption previously + if (lastLessonIdRef.current === currentLessonId && decryptedLessonIds[currentLessonId]) return; + + // Update the last processed lesson id + lastLessonIdRef.current = currentLessonId; + + if (hasAccess && paidCourse && !decryptedLessonIds[currentLessonId]) { + decryptCurrentLesson(); + } + }, [currentLessonId, hasAccess, paidCourse, decryptedLessonIds, decryptCurrentLesson]); + + return { + decryptionPerformed: isCurrentLessonDecrypted, + loading, + decryptedLessonIds + }; +}; + +export default useCourseDecryption; diff --git a/src/pages/course/[slug]/index.js b/src/pages/course/[slug]/index.js index b03be74..c4700e7 100644 --- a/src/pages/course/[slug]/index.js +++ b/src/pages/course/[slug]/index.js @@ -12,6 +12,7 @@ import { nip19 } from 'nostr-tools'; import { useToast } from '@/hooks/useToast'; import { ProgressSpinner } from 'primereact/progressspinner'; import { useDecryptContent } from '@/hooks/encryption/useDecryptContent'; +import useCourseDecryption from '@/hooks/encryption/useCourseDecryption'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import appConfig from '@/config/appConfig'; import useWindowWidth from '@/hooks/useWindowWidth'; @@ -135,153 +136,6 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => { return { lessons, uniqueLessons, setLessons }; }; -const useDecryption = (session, paidCourse, course, lessons, setLessons, router) => { - const [decryptedLessonIds, setDecryptedLessonIds] = useState({}); - const [loading, setLoading] = useState(false); - const { decryptContent } = useDecryptContent(); - const processingRef = useRef(false); - const lastLessonIdRef = useRef(null); - const retryCountRef = useRef({}); - const MAX_RETRIES = 3; - - // Get the current active lesson - const currentLessonIndex = router.query.active ? parseInt(router.query.active, 10) : 0; - const currentLesson = lessons.length > 0 ? lessons[currentLessonIndex] : null; - const currentLessonId = currentLesson?.id; - - // Check if the current lesson has been decrypted - const isCurrentLessonDecrypted = - !paidCourse || - (currentLessonId && decryptedLessonIds[currentLessonId]); - - // Check user access - const hasAccess = useMemo(() => { - if (!session?.user || !paidCourse || !course) return false; - - return ( - session.user.purchased?.some(purchase => purchase.courseId === course?.d) || - session.user?.role?.subscribed || - session.user?.pubkey === course?.pubkey - ); - }, [session, paidCourse, course]); - - // Reset retry count when lesson changes - useEffect(() => { - if (currentLessonId && lastLessonIdRef.current !== currentLessonId) { - retryCountRef.current[currentLessonId] = 0; - } - }, [currentLessonId]); - - // Simplified decrypt function - const decryptCurrentLesson = useCallback(async () => { - if (!currentLesson || !hasAccess || !paidCourse) return; - if (processingRef.current) return; - if (decryptedLessonIds[currentLesson.id]) return; - if (!currentLesson.content) return; - - // Check retry count - if (!retryCountRef.current[currentLesson.id]) { - retryCountRef.current[currentLesson.id] = 0; - } - - // Limit maximum retries - if (retryCountRef.current[currentLesson.id] >= MAX_RETRIES) { - return; - } - - // Increment retry count - retryCountRef.current[currentLesson.id]++; - - try { - processingRef.current = true; - setLoading(true); - - // Start the decryption process - const decryptionPromise = decryptContent(currentLesson.content); - - // Add safety timeout to prevent infinite processing - let timeoutId; - const timeoutPromise = new Promise((_, reject) => { - timeoutId = setTimeout(() => { - // Cancel the in-flight request when timeout occurs - if (decryptionPromise.cancel) { - decryptionPromise.cancel(); - } - reject(new Error('Decryption timeout')); - }, 10000); - }); - - // Use a separate try-catch for the race - let decryptedContent; - try { - // Race between decryption and timeout - decryptedContent = await Promise.race([ - decryptionPromise, - timeoutPromise - ]); - - // Clear the timeout if decryption wins - clearTimeout(timeoutId); - } catch (error) { - // If timeout or network error, schedule a retry - setTimeout(() => { - processingRef.current = false; - decryptCurrentLesson(); - }, 5000); - throw error; - } - - if (!decryptedContent) { - return; - } - - // Update the lessons array with decrypted content - const updatedLessons = lessons.map(lesson => - lesson.id === currentLesson.id - ? { ...lesson, content: decryptedContent } - : lesson - ); - - setLessons(updatedLessons); - - // Mark this lesson as decrypted - setDecryptedLessonIds(prev => ({ - ...prev, - [currentLesson.id]: true - })); - - // Reset retry counter on success - retryCountRef.current[currentLesson.id] = 0; - } catch (error) { - // Silent error handling to prevent UI disruption - } finally { - setLoading(false); - processingRef.current = false; - } - }, [currentLesson, hasAccess, paidCourse, decryptContent, lessons, setLessons, decryptedLessonIds]); - - // Run decryption when lesson changes - useEffect(() => { - if (!currentLessonId) return; - - // Skip if the lesson hasn't changed, unless it failed decryption previously - if (lastLessonIdRef.current === currentLessonId && decryptedLessonIds[currentLessonId]) return; - - // Update the last processed lesson id - lastLessonIdRef.current = currentLessonId; - - if (hasAccess && paidCourse && !decryptedLessonIds[currentLessonId]) { - decryptCurrentLesson(); - } - }, [currentLessonId, hasAccess, paidCourse, decryptedLessonIds, decryptCurrentLesson]); - - return { - decryptionPerformed: isCurrentLessonDecrypted, - loading, - decryptedLessonIds - }; -}; - const Course = () => { const router = useRouter(); const { ndk, addSigner } = useNDKContext(); @@ -371,7 +225,7 @@ const Course = () => { course?.pubkey ); - const { decryptionPerformed, loading: decryptionLoading, decryptedLessonIds } = useDecryption( + const { decryptionPerformed, loading: decryptionLoading, decryptedLessonIds } = useCourseDecryption( session, paidCourse, course,