);
- } 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])}
+
) : (