From 4b1f1a55f3c39fc664480382363005d22060bb01 Mon Sep 17 00:00:00 2001 From: benya7 Date: Mon, 28 Jul 2025 18:42:33 +0200 Subject: [PATCH 01/11] refactor: Standardize core types and remove static data --- .../components/admin/categoriesManagement.vue | 9 +- .../components/admin/contentManagement.vue | 7 +- .../components/admin/featuredManagement.vue | 9 +- .../admin/maintenanceManagement.vue | 14 +- .../admin/subscriptionManagement.vue | 34 +-- .../src/components/misc/contentCard.vue | 3 +- .../components/misc/infiniteReleaseList.vue | 4 +- .../src/components/releases/albumViewer.vue | 3 +- .../releases/contentCategoryForm.vue | 18 +- .../components/releases/metadataFieldForm.vue | 8 +- .../src/components/releases/releaseForm.vue | 30 +- .../renderer/src/composables/staticData.ts | 276 ------------------ .../src/constants/contentCategories.ts | 0 .../renderer/src/plugins/lensService/hooks.ts | 212 ++++---------- packages/renderer/src/types.ts | 17 +- packages/renderer/src/views/categoryPage.vue | 4 +- packages/renderer/src/views/homePage.vue | 5 +- packages/renderer/src/views/releasePage.vue | 4 +- 18 files changed, 135 insertions(+), 522 deletions(-) delete mode 100644 packages/renderer/src/composables/staticData.ts delete mode 100644 packages/renderer/src/constants/contentCategories.ts diff --git a/packages/renderer/src/components/admin/categoriesManagement.vue b/packages/renderer/src/components/admin/categoriesManagement.vue index 24498e17..a3b8351f 100644 --- a/packages/renderer/src/components/admin/categoriesManagement.vue +++ b/packages/renderer/src/components/admin/categoriesManagement.vue @@ -133,8 +133,8 @@ import { useSnackbarMessage } from '/@/composables/snackbarMessage'; import ContentCategoryForm from '/@/components/releases/contentCategoryForm.vue'; import confirmationDialog from '/@/components/misc/confimationDialog.vue'; -import type { ContentCategoryData, ContentCategoryMetadata } from '@riffcc/lens-sdk'; import { useContentCategoriesQuery } from '../../plugins/lensService/hooks'; +import type { ContentCategoryItem } from '/@/types'; const { data: contentCategories } = useContentCategoriesQuery(); @@ -143,12 +143,7 @@ const createCategoryDialog = ref(false); const editCategoryDialog = ref(false); const confirmDeleteCategoryDialog = ref(false); -const editedContentCategory = ref, 'siteAddress'>>({ - id: '', - displayName: '', - featured: false, - metadataSchema: {}, -}); +const editedContentCategory = ref>({}); const { snackbarMessage, showSnackbar, openSnackbar, closeSnackbar } = useSnackbarMessage(); diff --git a/packages/renderer/src/components/admin/contentManagement.vue b/packages/renderer/src/components/admin/contentManagement.vue index 297f6d52..18ebfa34 100644 --- a/packages/renderer/src/components/admin/contentManagement.vue +++ b/packages/renderer/src/components/admin/contentManagement.vue @@ -179,7 +179,6 @@ import { parseUrlOrCid, // getStatusColor, } from '/@/utils'; -import type { AnyObject } from '@riffcc/lens-sdk'; import { useDeleteReleaseMutation, useGetReleasesQuery } from '/@/plugins/lensService/hooks'; @@ -226,8 +225,8 @@ const tableHeaders: Header[] = [ { title: 'Actions', key: 'actions', sortable: false }, ]; -const targetReleaseToEdit = ref | null>(null); -const targetReleaseToDelete = ref | null>(null); +const targetReleaseToEdit = ref(null); +const targetReleaseToDelete = ref(null); const { snackbarMessage, showSnackbar, openSnackbar, closeSnackbar } = useSnackbarMessage(); @@ -243,7 +242,7 @@ function handleError(message: string) { async function confirmDeleteBlockRelease() { if (!targetReleaseToDelete.value) return; - deleteReleaseMutation.mutate({ id: targetReleaseToDelete.value.id }); + deleteReleaseMutation.mutate(targetReleaseToDelete.value.id); } function requestFeatureRelease(releaseId: string | undefined) { diff --git a/packages/renderer/src/components/admin/featuredManagement.vue b/packages/renderer/src/components/admin/featuredManagement.vue index 33a9b210..e360f208 100644 --- a/packages/renderer/src/components/admin/featuredManagement.vue +++ b/packages/renderer/src/components/admin/featuredManagement.vue @@ -160,12 +160,11 @@ import { computed, onMounted, ref, watch, type Ref } from 'vue'; import { useSnackbarMessage } from '/@/composables/snackbarMessage'; import confirmationDialog from '/@/components/misc/confimationDialog.vue'; import { filterActivedFeatured, filterPromotedFeatured } from '/@/utils'; -import type { FeaturedReleaseItem, PartialFeaturedReleaseItem } from '/@/types'; +import type { FeaturedReleaseItem } from '/@/types'; import { useAddFeaturedReleaseMutation, useEditFeaturedReleaseMutation, useGetFeaturedReleasesQuery } from '/@/plugins/lensService/hooks'; -import { FEATURED_END_TIME_PROPERTY } from '@riffcc/lens-sdk'; const props = defineProps<{ - initialFeatureData: PartialFeaturedReleaseItem | null; + initialFeatureData: Partial | null; }>(); const emit = defineEmits<{ @@ -196,7 +195,7 @@ const editFeaturedReleaseMutation = useEditFeaturedReleaseMutation({ }, }); -const newFeaturedRelease: Ref = ref({}); +const newFeaturedRelease: Ref> = ref({}); const formRef = ref(); const isLoading = computed(() => addFeaturedReleaseMutation.isPending.value || editFeaturedReleaseMutation.isPending.value); @@ -300,7 +299,7 @@ const confirmEndFeaturedRelease = async () => { if (!featuredItemIdToEnd.value) return; editFeaturedReleaseMutation.mutate({ ...featuredItemIdToEnd.value, - [FEATURED_END_TIME_PROPERTY]: (new Date()).toISOString(), + endTime: (new Date()).toISOString(), }); featuredItemIdToEnd.value = null; }; diff --git a/packages/renderer/src/components/admin/maintenanceManagement.vue b/packages/renderer/src/components/admin/maintenanceManagement.vue index 17189b8b..1992d886 100644 --- a/packages/renderer/src/components/admin/maintenanceManagement.vue +++ b/packages/renderer/src/components/admin/maintenanceManagement.vue @@ -116,7 +116,6 @@ import { ref } from 'vue'; import { useGetReleasesQuery, useGetFeaturedReleasesQuery, useAddReleaseMutation, useEditReleaseMutation, useDeleteReleaseMutation, useAddFeaturedReleaseMutation, useEditFeaturedReleaseMutation, useDeleteFeaturedReleaseMutation } from '/@/plugins/lensService/hooks'; import { useSnackbarMessage } from '/@/composables/snackbarMessage'; import type { ReleaseItem } from '/@/types'; -import type { AnyObject } from '@riffcc/lens-sdk'; const isExporting = ref(false); const isImporting = ref(false); @@ -236,7 +235,7 @@ const deleteAllData = async () => { console.log(`Deleting ${featuredReleases.value.length} featured releases...`); for (const featured of featuredReleases.value) { try { - const result = await deleteFeaturedReleaseMutation.mutateAsync({ id: featured.id }); + const result = await deleteFeaturedReleaseMutation.mutateAsync(featured.id); if (result.success) { featuredDeleted++; } else { @@ -253,7 +252,7 @@ const deleteAllData = async () => { console.log(`Deleting ${releases.value.length} releases...`); for (const release of releases.value) { try { - const result = await deleteReleaseMutation.mutateAsync({ id: release.id }); + const result = await deleteReleaseMutation.mutateAsync(release.id); if (result.success) { releasesDeleted++; } else { @@ -295,13 +294,15 @@ const performImport = async () => { for (const release of importData.releases) { try { // Extract the data without the __context - const releaseData: Omit, 'siteAddress'> = { + const releaseData: ReleaseItem = { id: release.id, name: release.name, categoryId: release.categoryId, contentCID: release.contentCID, thumbnailCID: release.thumbnailCID, metadata: release.metadata, + siteAddress: release.siteAddress, + postedBy: release.postedBy, }; if (importMode.value === 'upsert') { @@ -333,6 +334,9 @@ const performImport = async () => { for (const featured of importData.featuredReleases) { try { const featuredData = { + id: featured.id, + siteAddress: featured.siteAddress, + postedBy: featured.postedBy, releaseId: featured.releaseId, startTime: featured.startTime, endTime: featured.endTime, @@ -346,8 +350,6 @@ const performImport = async () => { // Update existing await editFeaturedReleaseMutation.mutateAsync({ ...featuredData, - id: featured.id, - siteAddress: existing.siteAddress, }); featuredImported++; } else { diff --git a/packages/renderer/src/components/admin/subscriptionManagement.vue b/packages/renderer/src/components/admin/subscriptionManagement.vue index 16593bbd..452b92de 100644 --- a/packages/renderer/src/components/admin/subscriptionManagement.vue +++ b/packages/renderer/src/components/admin/subscriptionManagement.vue @@ -18,15 +18,11 @@ @submit.prevent="handleOnSubmit" > - + @@ -83,7 +78,7 @@ icon="$delete" density="comfortable" size="small" - @click="() => unsubscribe({id: s.id})" + @click="() => unsubscribe({ id: s.id })" > @@ -106,9 +101,9 @@ diff --git a/packages/renderer/src/composables/lensInitialization.ts b/packages/renderer/src/composables/lensInitialization.ts new file mode 100644 index 00000000..fd32a21a --- /dev/null +++ b/packages/renderer/src/composables/lensInitialization.ts @@ -0,0 +1,74 @@ +import { ref } from 'vue'; +import { useLensService } from '/@/plugins/lensService/hooks'; +import { RIFFCC_PEERBIT_BOOTSTRAPPERS } from '../constants/config'; + +// This state will be shared across the entire application +const isLensReady = ref(false); + +const initLensService = async () => { + // Prevent re-initialization + if (isLensReady.value) return; + + const { lensService } = useLensService(); + const siteAddress = import.meta.env.VITE_SITE_ADDRESS; + const lensNode = import.meta.env.VITE_LENS_NODE; + + // --- Environment Variable Checks (Good, no changes needed) --- + if (!siteAddress) { + throw new Error('VITE_SITE_ADDRESS env var missing...', { cause: 'MISSING_CONFIG' }); + } + if (!lensNode) { + throw new Error('VITE_LENS_NODE env var missing...', { cause: 'MISSING_CONFIG' }); + } + + try { + console.log('Starting background Lens Service initialization...'); + await lensService.init('.lens-node'); + + // --- REFINED DIALING LOGIC --- + + // 1. Construct a clean, unified list of all addresses to dial. + // - `lensNode` is treated as a single string (no spread). + // - `RIFFCC_PEERBIT_BOOTSTRAPPERS` is correctly spread if it's an array. + const allAddressesToDial = [ + lensNode, + ...RIFFCC_PEERBIT_BOOTSTRAPPERS, + ].filter(Boolean); // .filter(Boolean) removes any empty or null strings. + + console.log('Attempting to dial bootstrap peers:', allAddressesToDial); + + // 2. Dial all peers in parallel. + const dialResults = await Promise.allSettled( + allAddressesToDial.map(b => lensService.peerbit?.dial(b.trim())), + ); + + // 3. (IMPROVEMENT) Log the results of the dialing attempts for easy debugging. + let successfulDials = 0; + dialResults.forEach((result, index) => { + const address = allAddressesToDial[index]; + if (result.status === 'fulfilled') { + successfulDials++; + console.log(`āœ… Successfully dialed: ${address}`); + } else { + console.warn(`āŒ Failed to dial: ${address}`, result.reason.message || result.reason); + } + }); + console.log(`Finished dialing: ${successfulDials}/${allAddressesToDial.length} peers connected.`); + + + // --- Continue with initialization --- + await lensService.openSite(siteAddress, { federate: false }); + isLensReady.value = true; + console.log('āœ… Lens Service is ready in the background.'); + + } catch (error) { + console.error('āŒ Lens Service background initialization failed:', error); + } +}; + +export function useLensInitialization() { + return { + isLensReady, + initLensService, + }; +} diff --git a/packages/renderer/src/constants/config.ts b/packages/renderer/src/constants/config.ts new file mode 100644 index 00000000..8401e22b --- /dev/null +++ b/packages/renderer/src/constants/config.ts @@ -0,0 +1,5 @@ +export const RIFFCC_IPFS_GATEWAY = 'cdn.riff.cc'; +export const RIFFCC_PEERBIT_BOOTSTRAPPERS = [ + '/dns4/4032881a26640025f9a4253104b7aaf6d4b55599.peerchecker.com/tcp/4003/wss/p2p/12D3KooWPYWLY5E7w1SyPJ18y77Wsyfo1fEJcwRonKNPxPam3teJ', + '/dns4/65da3760cb3fd2926532310b0650ddca4f88ebd5.peerchecker.com/tcp/4003/wss/p2p/12D3KooWMQTwyWnvKyFPjs72bbrDMUDM7pmtF328X7iTfWws3A18', +]; diff --git a/packages/renderer/src/constants/ipfs.ts b/packages/renderer/src/constants/ipfs.ts deleted file mode 100644 index 6f2cfd30..00000000 --- a/packages/renderer/src/constants/ipfs.ts +++ /dev/null @@ -1 +0,0 @@ -export const IPFS_GATEWAY = 'cdn.riff.cc'; diff --git a/packages/renderer/src/plugins/index.ts b/packages/renderer/src/plugins/index.ts index 4a6796a5..4636ed93 100644 --- a/packages/renderer/src/plugins/index.ts +++ b/packages/renderer/src/plugins/index.ts @@ -5,36 +5,8 @@ import type { App } from 'vue'; import vuetify from './vuetify'; import router from './router'; import lensServicePlugin from './lensService'; -import { VueQueryPlugin, QueryClient } from '@tanstack/vue-query'; - -// Configure QueryClient with optimized defaults for P2P network -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - // Network timeout for queries - PeerBit needs time to find nodes - networkMode: 'online', - // Retry configuration - retry: (failureCount, error) => { - // Handle PeerBit-specific errors - if (error?.message?.includes('delivery acknowledges from all nodes')) { - return failureCount < 2; // Limited retries for connectivity - } - if (error?.message?.includes('Failed to get message')) { - return failureCount < 3; // More retries for message delivery - } - return failureCount < 2; // Default retry - }, - retryDelay: attemptIndex => Math.min(500 * Math.pow(2, attemptIndex), 2000), - // Cache configuration - staleTime: 1000 * 60, // 1 minute default - gcTime: 1000 * 60 * 15, // 15 minutes - }, - mutations: { - retry: 1, - retryDelay: 1000, - }, - }, -}); +import { VueQueryPlugin } from '@tanstack/vue-query'; +import { queryClient } from './tanstackQuery'; export function registerPlugins (app: App) { app diff --git a/packages/renderer/src/plugins/router.ts b/packages/renderer/src/plugins/router.ts index e11bceb8..b43f5858 100644 --- a/packages/renderer/src/plugins/router.ts +++ b/packages/renderer/src/plugins/router.ts @@ -1,7 +1,10 @@ -import {createRouter, createWebHashHistory, type RouteRecordRaw} from 'vue-router'; - +import type { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'; +import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-router'; +import { multiaddr } from '@multiformats/multiaddr'; // Keep HomePage as direct import since it's the landing page import HomePage from '/@/views/homePage.vue'; +import { queryClient } from './tanstackQuery'; +import type { AccountStatusResponse } from '@riffcc/lens-sdk'; // Lazy load all other routes const AdminPage = () => import('../views/adminPage.vue'); @@ -13,6 +16,104 @@ const TermsPage = () => import('/@/views/termsPage.vue'); const UploadPage = () => import('/@/views/uploadPage.vue'); const CategoryPage = () => import('../views/categoryPage.vue'); +/** + * Parses the VITE_LENS_NODE environment variable to build the base API URL. + * This provides a single, reliable source of truth for the API endpoint. + * + * @returns {string} The constructed base API URL. + */ +function getApiUrl(): string { + const lensNodeMaStr = import.meta.env.VITE_LENS_NODE; + + if (!lensNodeMaStr) { + console.error('VITE_LENS_NODE environment variable is not set. API calls will fail.'); + // Fallback to a default or return an empty string, depending on desired behavior. + return 'http://localhost:5002/api/v1'; + } + + try { + const ma = multiaddr(lensNodeMaStr); + const nodeOptions = ma.nodeAddress(); // Gets { family, address, port } + + // Determine the protocol based on the presence of 'wss' or 'ws' + // 'wss' implies 'https' and 'ws' implies 'http' + const protocol = ma.getComponents().map(c => c.name).includes('wss') ? 'https' : 'http'; + + // Determine the API port. Conventionally, it might be different from the P2P port. + // Let's assume a convention: P2P port 8002 -> API port 5002, P2P 4003 -> API 9002 + let apiPort; + switch (nodeOptions.port) { + case 8002: // Local P2P port + apiPort = 5002; + break; + case 4003: // Production P2P port + apiPort = 9002; + break; + default: + // Default fallback if the P2P port is something unexpected + console.warn(`Unexpected P2P port ${nodeOptions.port}, defaulting API port to 9002.`); + apiPort = 9002; + } + + return `${protocol}://${nodeOptions.address}:${apiPort}/api/v1`; + + } catch (error) { + console.error('Failed to parse VITE_LENS_NODE multiaddr:', error); + // Fallback if parsing fails + return 'http://localhost:5002/api/v1'; + } +} + +export const API_URL = getApiUrl(); +const PREFETCH_CONFIG = { + initialReleases: { + url: `${API_URL}/releases`, + queryKey: ['releases'], + }, + initialFeaturedReleases: { + url: `${API_URL}/featured-releases`, + queryKey: ['featuredReleases'], + }, + initialContentCategories: { + url: `${API_URL}/content-categories`, + queryKey: ['contentCategories'], + }, +}; + +type PrefetchKey = keyof typeof PREFETCH_CONFIG; + +/** + * Checks if the user is authenticated and has the required permissions. + * Redirects to the homepage if checks fail. + * + * @param to - The route being navigated to. + * @param from - The route being navigated from. + * @param next - The navigation guard's next function. + * @param requiredPermission - The specific permission string to check for (e.g., 'release:create'). + */ +export function requirePermission( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + next: NavigationGuardNext, + requiredPermission: string, +) { + // 1. Synchronously get the account status from the cache. + const accountStatus = queryClient.getQueryData(['accountStatus']); + + // 2. Check for the permission. + // We use `?.` (optional chaining) to safely access `permissions` in case accountStatus is undefined. + const hasPermission = accountStatus?.permissions.includes(requiredPermission) ?? false; + + if (hasPermission) { + // 3. If the user has permission, allow them to proceed. + next(); + } else { + // 4. If the user does not have permission, redirect to the homepage. + console.warn(`Redirecting: User lacks required permission ('${requiredPermission}') for route '${to.path}'.`); + next({ path: '/' }); + } +} + const routes: Array = [ { path: '/', @@ -22,16 +123,31 @@ const routes: Array = [ path: '/account', name: 'Account', component: AccountPage, + }, { path: '/upload', name: 'Upload', component: UploadPage, + beforeEnter: (to, from, next) => { + // We pass the specific permission required for this route + requirePermission(to, from, next, 'release:create'); + }, }, { path: '/admin', name: 'Admin Website', component: AdminPage, + beforeEnter: (to, from, next) => { + // Example for a more complex check. You would create a dedicated helper for this. + const accountStatus = queryClient.getQueryData(['accountStatus']); + const isAdmin = accountStatus?.isAdmin || accountStatus?.roles.includes('moderator') || false; + if (isAdmin) { + next(); + } else { + next({ path: '/' }); + } + }, }, { path: '/about', @@ -50,25 +166,129 @@ const routes: Array = [ name: 'Release', component: ReleasePage, props: true, + beforeEnter: async (to, from, next) => { + // 1. Get the release ID from the route params. + const id = to.params.id as string; + + // Ensure we have an ID to work with. + if (!id) { + console.error('Release page navigation attempted without an ID.'); + // Optionally, redirect to a 404 page or the homepage. + next({ path: '/' }); + return; + } + + console.log(`Release Guard: Pre-fetching data for release ID: ${id}`); + + // 2. Define the specific query key for this release. + const queryKey = ['release', id]; + + // 3. Check if data for this specific release is already in the cache. + // This is a crucial optimization. If the user clicks a release, then navigates + // away and clicks the same release again, we don't need to re-fetch. + if (queryClient.getQueryData(queryKey)) { + console.log(`Cache hit for release ${id}. Skipping fetch.`); + next(); + return; + } + + try { + // 4. Fetch the data from your fast API. + const response = await fetch(`${API_URL}/releases/${id}`); + + if (response.ok) { + const releaseData = await response.json(); + + // 5. Seed the cache with the fetched data. + queryClient.setQueryData(queryKey, releaseData); + console.log(`āœ… Cache seeded for release ${id}.`); + } else { + // Handle cases where the release is not found (404) or other server errors. + console.error(`API Error fetching release ${id}: Status ${response.status}`); + // You might want to clear any stale data if it exists and redirect. + queryClient.setQueryData(queryKey, undefined); + // Optionally redirect to a 'not-found' page. + // For now, we'll just proceed and let the component handle the empty state. + } + } catch (error) { + console.error(`Fetch failed for release ${id}:`, error); + } finally { + // 6. Always call next() to allow navigation to proceed. + next(); + } + }, }, { path: '/featured/:category', component: CategoryPage, - props: true, - }, - { - path: '/:category', - component: CategoryPage, props: route => ({ ...route.params, showAll: true }), }, ]; -const routeur = createRouter({ +const router = createRouter({ history: createWebHashHistory(), routes, scrollBehavior() { - return {top: 0}; + return { top: 0 }; }, }); -export default routeur; +let hasAttemptedPrefetch = false; + +router.beforeEach(async (to, from, next) => { + if (hasAttemptedPrefetch) { + next(); + return; + } + + console.log('Global Guard: First-time app entry. Checking API health for pre-fetching...'); + hasAttemptedPrefetch = true; // Set this immediately to ensure this entire block runs only once. + + try { + // 1. Perform a quick health check with a short timeout. + // We use AbortController to enforce a timeout on the fetch call. + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 1500); // 1.5-second timeout + + const healthResponse = await fetch(`${API_URL}/health`, { signal: controller.signal }); + clearTimeout(timeoutId); // Clear the timeout if the fetch completes in time + + if (!healthResponse.ok) { + throw new Error(`API health check failed with status: ${healthResponse.status}`); + } + + // 2. If health check passes, proceed with pre-fetching. + console.log('API is healthy. Proceeding with pre-fetching and cache seeding...'); + const prefetchKeys = Object.keys(PREFETCH_CONFIG) as PrefetchKey[]; + const promises = prefetchKeys.map(key => fetch(PREFETCH_CONFIG[key].url)); + const results = await Promise.allSettled(promises); + + console.groupCollapsed('API Pre-fetch Responses & Cache Seeding'); + // Process each result + results.forEach(async (result, index) => { + const key = prefetchKeys[index]; + const config = PREFETCH_CONFIG[key]; + + if (result.status === 'fulfilled' && result.value.ok) { + const data = await result.value.json(); + queryClient.setQueryData(config.queryKey, data); + console.log(`āœ… [SUCCESS & CACHE SEEDED] ${key}:`, data); + } else if (result.status === 'fulfilled' && !result.value.ok) { + console.error(`āŒ [API ERROR] for ${key} at ${result.value.url}: Server responded with status ${result.value.status}`); + } else if (result.status === 'rejected') { + console.error(`āŒ [FETCH FAILED] for ${key}:`, result.reason.message); + } + }); + + console.groupEnd(); + + } catch (error) { + console.error('An unexpected error occurred during global data pre-fetching:', error); + } finally { + // ALWAYS call next() to allow the initial navigation to complete. + next(); + } +}); + + +export default router; diff --git a/packages/renderer/src/plugins/tanstackQuery.ts b/packages/renderer/src/plugins/tanstackQuery.ts new file mode 100644 index 00000000..4a048e93 --- /dev/null +++ b/packages/renderer/src/plugins/tanstackQuery.ts @@ -0,0 +1,30 @@ +import { QueryClient } from '@tanstack/vue-query'; + +// Configure QueryClient with optimized defaults for P2P network +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Network timeout for queries - PeerBit needs time to find nodes + networkMode: 'online', + // Retry configuration + retry: (failureCount, error) => { + // Handle PeerBit-specific errors + if (error?.message?.includes('delivery acknowledges from all nodes')) { + return failureCount < 2; // Limited retries for connectivity + } + if (error?.message?.includes('Failed to get message')) { + return failureCount < 3; // More retries for message delivery + } + return failureCount < 2; // Default retry + }, + retryDelay: attemptIndex => Math.min(500 * Math.pow(2, attemptIndex), 2000), + // Cache configuration + staleTime: 1000 * 60, // 1 minute default + gcTime: 1000 * 60 * 15, // 15 minutes + }, + mutations: { + retry: 1, + retryDelay: 1000, + }, + }, +}); diff --git a/packages/renderer/src/utils.ts b/packages/renderer/src/utils.ts index 12fb3b2f..e352f46c 100644 --- a/packages/renderer/src/utils.ts +++ b/packages/renderer/src/utils.ts @@ -1,7 +1,7 @@ import {base16} from 'multiformats/bases/base16'; import {CID} from 'multiformats/cid'; import {cid as isCID} from 'is-ipfs'; -import { IPFS_GATEWAY } from './constants/ipfs'; +import { RIFFCC_IPFS_GATEWAY } from './constants/config'; import type { FeaturedReleaseItem } from './types'; import {Duration} from 'luxon'; @@ -82,7 +82,7 @@ export function parseUrlOrCid(urlOrCid?: string): string | undefined { // Load optional gateway override from environment variable const gatewayOverride = import.meta.env.VITE_IPFS_GATEWAY as string | undefined; - const selectedGateway = gatewayOverride || IPFS_GATEWAY; + const selectedGateway = gatewayOverride || RIFFCC_IPFS_GATEWAY; // Use HTTPS for gateways, unless the override specifies a protocol const gatewayBase = selectedGateway.startsWith('http://') || selectedGateway.startsWith('https://') @@ -94,7 +94,7 @@ export function parseUrlOrCid(urlOrCid?: string): string | undefined { // For now, let's assume the override is a domain or IP:port. const codexGatewayBase = gatewayOverride ? (gatewayOverride.startsWith('http://') || gatewayOverride.startsWith('https://') ? gatewayOverride.replace(/^(https?:\/\/)/, '$1codex-') : `https://codex-${gatewayOverride}`) - : `https://codex-${IPFS_GATEWAY}`; + : `https://codex-${RIFFCC_IPFS_GATEWAY}`; if (urlOrCid.startsWith('zD')) { diff --git a/packages/renderer/src/views/homePage.vue b/packages/renderer/src/views/homePage.vue index 76666619..e5c6389c 100644 --- a/packages/renderer/src/views/homePage.vue +++ b/packages/renderer/src/views/homePage.vue @@ -83,24 +83,17 @@ import { filterActivedFeatured, filterPromotedFeatured } from '../utils'; const router = useRouter(); -// Optimize loading: Reduce stale time for faster fallback and eager loading const { data: releases, isLoading: isReleasesLoading, isFetched: isReleasesFetched, -} = useGetReleasesQuery({ - staleTime: 1000 * 30, // 30s stale time for faster refresh -}); +} = useGetReleasesQuery(); const { data: featuredReleases, isLoading: isFeaturedReleasesLoading, isFetched: isFeaturedReleasesFetched, -} = useGetFeaturedReleasesQuery({ - staleTime: 1000 * 30, // 30s stale time for faster refresh -}); - - +} = useGetFeaturedReleasesQuery(); const { data: contentCategories } = useContentCategoriesQuery(); @@ -122,90 +115,47 @@ const promotedFeaturedReleases = computed(() => { }); +const activeSections = computed(() => { + const limitPerCategory = 8; + if (!contentCategories.value || !activedFeaturedReleases.value) return []; -function categorizeReleasesByFeaturedCategories( - rels?: ReleaseItem[], - featuredCats?: Omit, 'siteAddress'>[], - limitPerCategory: number = 8, -): Record[]> { - const result: Record[]> = {}; - if (!rels || !featuredCats) { - return result; - } - const addedReleaseIds = new Set(); - - featuredCats.forEach(fc => { - result[fc.id] = []; - }); - - for (const rel of rels) { - if (!rel.id || addedReleaseIds.has(rel.id)) { - continue; - } - - for (const fc of featuredCats) { - const currentCategoryId = fc.id; - if (rel.categoryId === currentCategoryId) { - if (result[currentCategoryId].length < limitPerCategory) { - result[currentCategoryId].push(rel); - addedReleaseIds.add(rel.id); - } - // A release is categorized, move to the next release. - // It won't be added to multiple sections by this logic as release.category is singular. - break; - } + const releasesByCategory = new Map(); + for (const release of activedFeaturedReleases.value) { + if (!release.categoryId) continue; + if (!releasesByCategory.has(release.categoryId)) { + releasesByCategory.set(release.categoryId, []); } + releasesByCategory.get(release.categoryId)!.push(release); } - return result; -} - -const categorizedReleases = computed(() => { - return categorizeReleasesByFeaturedCategories(activedFeaturedReleases.value, contentCategories.value); -}); - -const activeSections = computed<{ - id: string; - title: string; - items: ReleaseItem[]; - navigationPath: string; -}[]>(() => { - if (!contentCategories.value) return []; return contentCategories.value - .filter(c => c.featured) - .map(fc => { - const categoryId = fc.id; - const items = categorizedReleases.value[categoryId] || []; + .filter(category => category.featured) + .map(featuredCategory => { + const items = releasesByCategory.get(featuredCategory.categoryId) || []; return { - id: fc.id, - title: categoryId === 'tvShow' ? fc.displayName : `Featured ${fc.displayName}`, - items: items, - navigationPath: `/featured/${categoryId}`, + id: featuredCategory.categoryId, + title: featuredCategory.categoryId === 'tv-shows' ? featuredCategory.displayName : `Featured ${featuredCategory.displayName}`, + items: items.slice(0, limitPerCategory), + navigationPath: `/featured/${featuredCategory.categoryId}`, }; }) .filter(section => section.items.length > 0); }); - -// Progressive loading - show content as each query completes const isLoading = computed(() => { - // Show loading if BOTH queries are still loading - // This prevents showing "no featured content" before releases are loaded return isReleasesLoading.value || isFeaturedReleasesLoading.value; }); const noFeaturedContent = computed(() => { - // Only show "no featured content" if BOTH queries are done and there's no featured content if (!isReleasesFetched.value || !isFeaturedReleasesFetched.value) { - return false; // Still loading, don't show "no featured content" yet + return false; } return promotedFeaturedReleases.value.length === 0 && activeSections.value.length === 0; }); const noContent = computed(() => { - // Only show "no content" if BOTH queries are done and there's truly no content anywhere if (!isReleasesFetched.value || !isFeaturedReleasesFetched.value) { - return false; // Still loading something, don't show "no content" yet + return false; } return releases.value?.length === 0 && featuredReleases.value?.length === 0; }); diff --git a/types/env.d.ts b/types/env.d.ts index 05fc3dc5..a76b4eb2 100644 --- a/types/env.d.ts +++ b/types/env.d.ts @@ -20,7 +20,7 @@ interface ImportMetaEnv { readonly VITE_APP_VERSION: string; readonly VITE_SITE_ADDRESS: string | undefined; - readonly VITE_BOOTSTRAPPERS: string | undefined; + readonly VITE_LENS_NODE: string | undefined; } From 8e9f7a99a3a0e30ce203b77254f03485e151aaae Mon Sep 17 00:00:00 2001 From: benya7 Date: Tue, 29 Jul 2025 22:30:11 +0200 Subject: [PATCH 03/11] feat: Refactor UI components for new auth model and data flow --- .../renderer/src/components/layout/appBar.vue | 47 ++++-- .../src/components/layout/appFooter.vue | 4 +- .../renderer/src/plugins/lensService/hooks.ts | 1 - packages/renderer/src/views/accountPage.vue | 156 ++++++++++++------ packages/renderer/src/views/categoryPage.vue | 13 +- 5 files changed, 148 insertions(+), 73 deletions(-) diff --git a/packages/renderer/src/components/layout/appBar.vue b/packages/renderer/src/components/layout/appBar.vue index 5f8f220c..5cfd163d 100644 --- a/packages/renderer/src/components/layout/appBar.vue +++ b/packages/renderer/src/components/layout/appBar.vue @@ -36,20 +36,20 @@ :key="item.id" :title="item.displayName" active-class="text-primary-lighten-1" - :active="route.path === item.id" - @click="router.push(`/${item.id}`)" + :active="route.path === item.categoryId" + @click="router.push(`/featured/${item.categoryId}`)" >