course decryption logic in its own hook

This commit is contained in:
austinkelsay 2025-05-11 11:14:54 -05:00
parent c73d6eb8bb
commit f526913f30
No known key found for this signature in database
GPG Key ID: 5A763922E5BA08EE
2 changed files with 153 additions and 148 deletions

View File

@ -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;

View File

@ -12,6 +12,7 @@ import { nip19 } from 'nostr-tools';
import { useToast } from '@/hooks/useToast'; import { useToast } from '@/hooks/useToast';
import { ProgressSpinner } from 'primereact/progressspinner'; import { ProgressSpinner } from 'primereact/progressspinner';
import { useDecryptContent } from '@/hooks/encryption/useDecryptContent'; import { useDecryptContent } from '@/hooks/encryption/useDecryptContent';
import useCourseDecryption from '@/hooks/encryption/useCourseDecryption';
import ZapThreadsWrapper from '@/components/ZapThreadsWrapper'; import ZapThreadsWrapper from '@/components/ZapThreadsWrapper';
import appConfig from '@/config/appConfig'; import appConfig from '@/config/appConfig';
import useWindowWidth from '@/hooks/useWindowWidth'; import useWindowWidth from '@/hooks/useWindowWidth';
@ -135,153 +136,6 @@ const useLessons = (ndk, fetchAuthor, lessonIds, pubkey) => {
return { lessons, uniqueLessons, setLessons }; 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 Course = () => {
const router = useRouter(); const router = useRouter();
const { ndk, addSigner } = useNDKContext(); const { ndk, addSigner } = useNDKContext();
@ -371,7 +225,7 @@ const Course = () => {
course?.pubkey course?.pubkey
); );
const { decryptionPerformed, loading: decryptionLoading, decryptedLessonIds } = useDecryption( const { decryptionPerformed, loading: decryptionLoading, decryptedLessonIds } = useCourseDecryption(
session, session,
paidCourse, paidCourse,
course, course,