UNPKG

@replyke/core

Version:

Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.

309 lines 12.5 kB
import { createSlice, createSelector } from "@reduxjs/toolkit"; // Default state for a new entity list const createDefaultEntityListState = () => ({ entities: [], page: 1, loading: true, hasMore: true, error: null, lastFetched: null, // Default configuration sourceId: null, spaceId: null, limit: 10, include: null, // Default filters (user-controlled only) sortBy: "hot", sortByReaction: "upvote", sortDir: null, sortType: "auto", timeFrame: null, userId: null, followedOnly: false, keywordsFilters: null, titleFilters: null, contentFilters: null, attachmentsFilters: null, locationFilters: null, metadataFilters: null, }); // Initial state const initialState = { lists: {}, }; // Create the slice export const entityListsSlice = createSlice({ name: "entityLists", initialState, reducers: { // Initialize or get existing list initializeList: (state, action) => { const { listId } = action.payload; if (!state.lists[listId]) { state.lists[listId] = createDefaultEntityListState(); } }, // Update filters and sort configuration updateFiltersAndSortConfig: (state, action) => { const { listId, filters, sort, config, options } = action.payload; // Ensure list exists if (!state.lists[listId]) { state.lists[listId] = createDefaultEntityListState(); } const list = state.lists[listId]; // Handle resetFilters flag - reset only filter properties if (options?.resetFilters) { const defaultState = createDefaultEntityListState(); list.timeFrame = defaultState.timeFrame; list.userId = defaultState.userId; list.followedOnly = defaultState.followedOnly; list.keywordsFilters = defaultState.keywordsFilters; list.titleFilters = defaultState.titleFilters; list.contentFilters = defaultState.contentFilters; list.attachmentsFilters = defaultState.attachmentsFilters; list.locationFilters = defaultState.locationFilters; list.metadataFilters = defaultState.metadataFilters; } // Handle resetSort flag - reset only sort properties if (options?.resetSort) { const defaultState = createDefaultEntityListState(); list.sortBy = defaultState.sortBy; list.sortByReaction = defaultState.sortByReaction; list.sortDir = defaultState.sortDir; list.sortType = defaultState.sortType; } // Apply specified filters Object.keys(filters).forEach((key) => { if (filters[key] !== undefined) { list[key] = filters[key]; } }); // Apply specified sort configuration if (sort) { if (sort.sortBy !== undefined) list.sortBy = sort.sortBy; if (sort.sortByReaction !== undefined) list.sortByReaction = sort.sortByReaction; if (sort.sortDir !== undefined) list.sortDir = sort.sortDir; if (sort.sortType !== undefined) list.sortType = sort.sortType; } // Update config if provided if (config) { if (config.sourceId !== undefined) { list.sourceId = config.sourceId; } if (config.spaceId !== undefined) { list.spaceId = config.spaceId; } if (config.limit !== undefined) { list.limit = config.limit; } if (config.include !== undefined) { list.include = config.include; } } // Reset pagination when filters or sort changes list.page = 1; list.hasMore = true; list.error = null; }, // Set loading state for entity list setEntityListLoading: (state, action) => { const { listId, loading } = action.payload; if (state.lists[listId]) { state.lists[listId].loading = loading; } }, // Set entities for entity list setEntityListEntities: (state, action) => { const { listId, entities, append = false } = action.payload; if (!state.lists[listId]) { state.lists[listId] = createDefaultEntityListState(); } const list = state.lists[listId]; if (append) { // Filter out duplicates when appending const existingIds = new Set(list.entities.map((e) => e.id)); const newEntities = entities.filter((e) => !existingIds.has(e.id)); list.entities = [...list.entities, ...newEntities]; } else { list.entities = entities; } list.loading = false; list.lastFetched = Date.now(); // Note: hasMore is set explicitly by the caller based on limit from hook props }, // Increment page for load more incrementPage: (state, action) => { const listId = action.payload; if (state.lists[listId]) { state.lists[listId].page += 1; } }, // Set hasMore for entity list setEntityListHasMore: (state, action) => { const { listId, hasMore } = action.payload; if (state.lists[listId]) { state.lists[listId].hasMore = hasMore; } }, // Set error for entity list setEntityListError: (state, action) => { const { listId, error } = action.payload; if (state.lists[listId]) { state.lists[listId].error = error; state.lists[listId].loading = false; } }, // Add entity addEntity: (state, action) => { const { listId, entity, insertPosition = "first" } = action.payload; if (!state.lists[listId]) return; const list = state.lists[listId]; if (insertPosition === "last") { list.entities.push(entity); } else { list.entities.unshift(entity); } }, // Remove entity removeEntity: (state, action) => { const { listId, entityId } = action.payload; if (!state.lists[listId]) return; const list = state.lists[listId]; list.entities = list.entities.filter((e) => e.id !== entityId); }, // Update keywords filters (special case due to complexity) updateKeywordsFilters: (state, action) => { const { listId, type, key, value } = action.payload; if (!state.lists[listId]) { state.lists[listId] = createDefaultEntityListState(); } const list = state.lists[listId]; const items = Array.isArray(value) ? value : value ? [value] : []; let newFilters = list.keywordsFilters || {}; switch (type) { case "add": { if (key === "both") break; // Invalid to add to both newFilters = { ...newFilters, [key]: Array.from(new Set([...(newFilters[key] || []), ...items])), }; break; } case "remove": { if (key === "both") { newFilters = { includes: (newFilters.includes || []).filter((item) => !items.includes(item)), doesNotInclude: (newFilters.doesNotInclude || []).filter((item) => !items.includes(item)), }; } else { newFilters = { ...newFilters, [key]: (newFilters[key] || []).filter((item) => !items.includes(item)), }; } break; } case "reset": { if (key === "both") { newFilters = {}; } else { newFilters = { ...newFilters, [key]: undefined, }; } break; } case "replace": { if (key === "both") break; // Replace does not apply to both newFilters = { ...newFilters, [key]: items, }; break; } } list.keywordsFilters = Object.keys(newFilters).length > 0 ? newFilters : null; // Reset pagination when filters change list.page = 1; list.hasMore = true; list.error = null; }, // Clean up unused lists (for memory management) cleanupList: (state, action) => { const listId = action.payload; delete state.lists[listId]; }, // Clean up old lists (older than TTL) cleanupOldLists: (state, action) => { const ttl = action.payload; // TTL in milliseconds const now = Date.now(); Object.keys(state.lists).forEach((listId) => { const list = state.lists[listId]; if (list.lastFetched && now - list.lastFetched > ttl) { delete state.lists[listId]; } }); }, }, }); // Export actions export const { initializeList, updateFiltersAndSortConfig, setEntityListLoading, setEntityListEntities, incrementPage, setEntityListHasMore, setEntityListError, addEntity, removeEntity, updateKeywordsFilters, cleanupList, cleanupOldLists, } = entityListsSlice.actions; // Base selectors - use namespaced state for dual-mode support const selectEntityListsState = (state) => state.replyke.entityLists; const selectListId = (_, listId) => listId; // Memoized selectors using createSelector export const selectEntityList = createSelector([selectEntityListsState, selectListId], (entityListsState, listId) => entityListsState.lists[listId]); export const selectEntityListEntities = createSelector([selectEntityList], (entityList) => entityList?.entities || []); export const selectEntityListLoading = createSelector([selectEntityList], (entityList) => entityList?.loading || false); export const selectEntityListHasMore = createSelector([selectEntityList], (entityList) => entityList?.hasMore || false); export const selectEntityListSort = createSelector([selectEntityList], (entityList) => { if (!entityList) return null; return { sortBy: entityList.sortBy, sortByReaction: entityList.sortByReaction, sortDir: entityList.sortDir, sortType: entityList.sortType, }; }); export const selectEntityListFilters = createSelector([selectEntityList], (entityList) => { if (!entityList) return null; return { timeFrame: entityList.timeFrame, userId: entityList.userId, followedOnly: entityList.followedOnly, keywordsFilters: entityList.keywordsFilters, titleFilters: entityList.titleFilters, contentFilters: entityList.contentFilters, attachmentsFilters: entityList.attachmentsFilters, locationFilters: entityList.locationFilters, metadataFilters: entityList.metadataFilters, }; }); export const selectEntityListConfig = createSelector([selectEntityList], (entityList) => { if (!entityList) return null; return { sourceId: entityList.sourceId, spaceId: entityList.spaceId, limit: entityList.limit, include: entityList.include, }; }); export default entityListsSlice.reducer; //# sourceMappingURL=entityListsSlice.js.map