diff --git a/src/components/content/courses/CombinedLesson.js b/src/components/content/courses/CombinedLesson.js index ba4eeab..d837973 100644 --- a/src/components/content/courses/CombinedLesson.js +++ b/src/components/content/courses/CombinedLesson.js @@ -167,10 +167,17 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple }, [videoCompleted, videoTracking, lesson.id, setCompleted, isVideo]); const renderContent = () => { - if (isPaid && decryptionPerformed) { + if (!lesson?.content) { + if (isVideo) { + return ( +
+

No content available for this lesson.

+
+ ); + } return ( -
- +
+

No content available for this lesson.

); } @@ -209,15 +216,11 @@ const CombinedLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple ); } - if (lesson?.content) { - return ( -
- -
- ); - } - - return null; + return ( +
+ +
+ ); }; return ( diff --git a/src/components/content/courses/CourseSidebar.js b/src/components/content/courses/CourseSidebar.js index a757233..189e7f1 100644 --- a/src/components/content/courses/CourseSidebar.js +++ b/src/components/content/courses/CourseSidebar.js @@ -36,7 +36,10 @@ const CourseSidebar = ({ } ${isMobileView ? 'mb-3' : 'mb-2'} `} - onClick={() => onLessonSelect(index)} + onClick={() => { + // Force full page refresh to trigger proper decryption + window.location.href = `/course/${window.location.pathname.split('/').pop()}?active=${index}`; + }} >
{lesson.image && ( diff --git a/src/components/content/courses/DocumentLesson.js b/src/components/content/courses/DocumentLesson.js index 8af1ba7..d13cb49 100644 --- a/src/components/content/courses/DocumentLesson.js +++ b/src/components/content/courses/DocumentLesson.js @@ -113,9 +113,14 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple }, [isCompleted, lesson.id, setCompleted, isTracking]); const renderContent = () => { - if (isPaid && decryptionPerformed) { - return ; + if (!lesson?.content) { + return ( +
+

No content available for this lesson.

+
+ ); } + if (isPaid && !decryptionPerformed) { return (
@@ -128,10 +133,8 @@ const DocumentLesson = ({ lesson, course, decryptionPerformed, isPaid, setComple
); } - if (lesson?.content) { - return ; - } - return null; + + return ; }; return ( diff --git a/src/components/content/courses/VideoLesson.js b/src/components/content/courses/VideoLesson.js index 326ff53..1a704e0 100644 --- a/src/components/content/courses/VideoLesson.js +++ b/src/components/content/courses/VideoLesson.js @@ -163,13 +163,17 @@ const VideoLesson = ({ lesson, course, decryptionPerformed, isPaid, setCompleted }, [decryptionPerformed, isPaid, checkDuration]); const renderContent = () => { - if (isPaid && decryptionPerformed) { + // Content not available + if (!lesson?.content) { return ( -
- +
+

No content available for this lesson.

); - } else if (isPaid && !decryptionPerformed) { + } + + // Paid content that needs to be purchased + if (isPaid && !decryptionPerformed) { return (
); - } else if (lesson?.content) { - return ( -
- -
- ); } - return null; + + // Content is available and decrypted (or free) + return ( +
+ +
+ ); }; return ( diff --git a/src/config/appConfig.js b/src/config/appConfig.js index a8325ba..4363377 100644 --- a/src/config/appConfig.js +++ b/src/config/appConfig.js @@ -11,7 +11,8 @@ const appConfig = { ], authorPubkeys: [ 'f33c8a9617cb15f705fc70cd461cfd6eaf22f9e24c33eabad981648e5ec6f741', - 'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345' + 'c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345', + '6260f29fa75c91aaa292f082e5e87b438d2ab4fdf96af398567b01802ee2fcd4' ], customLightningAddresses: [ { diff --git a/src/hooks/encryption/useDecryptContent.js b/src/hooks/encryption/useDecryptContent.js index 5584f02..ae12ec6 100644 --- a/src/hooks/encryption/useDecryptContent.js +++ b/src/hooks/encryption/useDecryptContent.js @@ -1,30 +1,63 @@ -import { useState } from 'react'; +import { useState, useRef } from 'react'; import axios from 'axios'; export const useDecryptContent = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - - const decryptContent = async encryptedContent => { - setIsLoading(true); - setError(null); - - try { - const response = await axios.post('/api/decrypt', { encryptedContent }); - - if (response.status !== 200) { - throw new Error('Failed to decrypt content'); - } - - const decryptedContent = response.data.decryptedContent; - setIsLoading(false); - return decryptedContent; - } catch (err) { - setError(err.message); - setIsLoading(false); + const inProgressRef = useRef(false); + const cachedResults = useRef({}); + + const decryptContent = async (encryptedContent) => { + // Validate input + if (!encryptedContent) { return null; } + + // Prevent multiple simultaneous calls + if (inProgressRef.current) { + // Wait for a small delay to prevent tight loop + await new Promise(resolve => setTimeout(resolve, 100)); + + // Return a cached result if we have one + const firstChars = encryptedContent.substring(0, 20); + if (cachedResults.current[firstChars]) { + return cachedResults.current[firstChars]; + } + + return null; + } + + // Check if we've already decrypted this content + const firstChars = encryptedContent.substring(0, 20); + if (cachedResults.current[firstChars]) { + return cachedResults.current[firstChars]; + } + + try { + inProgressRef.current = true; + setIsLoading(true); + setError(null); + + const response = await axios.post('/api/decrypt', { encryptedContent }); + + if (response.status !== 200) { + throw new Error(`Failed to decrypt: ${response.statusText}`); + } + + const decryptedContent = response.data.decryptedContent; + + // Cache the result + cachedResults.current[firstChars] = decryptedContent; + + return decryptedContent; + } catch (error) { + setError(error.message || 'Decryption failed'); + return null; + } finally { + setIsLoading(false); + inProgressRef.current = false; + } }; - + return { decryptContent, isLoading, error }; }; diff --git a/src/pages/course/[slug]/index.js b/src/pages/course/[slug]/index.js index 22de673..49c384d 100644 --- a/src/pages/course/[slug]/index.js +++ b/src/pages/course/[slug]/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import { useRouter } from 'next/router'; import { parseCourseEvent, parseEvent, findKind0Fields } from '@/utils/nostr'; import CourseDetails from '@/components/content/courses/CourseDetails'; @@ -135,42 +135,102 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => { return { lessons, uniqueLessons, setLessons }; }; -const useDecryption = (session, paidCourse, course, lessons, setLessons) => { - const [decryptionPerformed, setDecryptionPerformed] = useState(false); - const [loading, setLoading] = useState(true); +const useDecryption = (session, paidCourse, course, lessons, setLessons, router) => { + const [decryptedLessonIds, setDecryptedLessonIds] = useState({}); + const [loading, setLoading] = useState(false); const { decryptContent } = useDecryptContent(); - - useEffect(() => { - const decrypt = async () => { - if (session?.user && paidCourse && !decryptionPerformed) { - setLoading(true); - const canAccess = - session.user.purchased?.some(purchase => purchase.courseId === course?.d) || - session.user?.role?.subscribed || - session.user?.pubkey === course?.pubkey; - - if (canAccess && lessons.length > 0) { - try { - const decryptedLessons = await Promise.all( - lessons.map(async lesson => { - const decryptedContent = await decryptContent(lesson.content); - return { ...lesson, content: decryptedContent }; - }) - ); - setLessons(decryptedLessons); - setDecryptionPerformed(true); - } catch (error) { - console.error('Error decrypting lessons:', error); - } - } - setLoading(false); + const processingRef = useRef(false); + const lastLessonIdRef = useRef(null); + + // 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]); + + // 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; + + try { + processingRef.current = true; + setLoading(true); + + // Add safety timeout to prevent infinite processing + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Decryption timeout')), 10000) + ); + + // Race between decryption and timeout + const decryptedContent = await Promise.race([ + decryptContent(currentLesson.content), + timeoutPromise + ]); + + 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 + })); + } catch (error) { + // Silent error handling to prevent UI disruption + } finally { setLoading(false); - }; - decrypt(); - }, [session, paidCourse, course, lessons, decryptionPerformed, setLessons]); - - return { decryptionPerformed, loading }; + 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 + if (lastLessonIdRef.current === 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 = () => { @@ -262,12 +322,13 @@ const Course = () => { course?.pubkey ); - const { decryptionPerformed, loading: decryptionLoading } = useDecryption( + const { decryptionPerformed, loading: decryptionLoading, decryptedLessonIds } = useDecryption( session, paidCourse, course, lessons, - setLessons + setLessons, + router ); useEffect(() => { @@ -478,23 +539,27 @@ const Course = () => { } const renderLesson = lesson => { + if (!lesson) return null; + + // Check if this specific lesson is decrypted + const lessonDecrypted = !paidCourse || decryptedLessonIds[lesson.id] || false; + if (lesson.topics?.includes('video') && lesson.topics?.includes('document')) { return ( - ); } else if (lesson.type === 'video' && !lesson.topics?.includes('document')) { return ( @@ -504,7 +569,7 @@ const Course = () => { @@ -545,7 +610,9 @@ const Course = () => {
{uniqueLessons.length > 0 && uniqueLessons[activeIndex] ? (
- {renderLesson(uniqueLessons[activeIndex])} +
+ {renderLesson(uniqueLessons[activeIndex])} +
) : (