diff --git a/app.json b/app.json index 6a1cdecc..a874bffd 100644 --- a/app.json +++ b/app.json @@ -17,16 +17,24 @@ "supportsTablet": true }, "android": { + "package": "org.calblueprint.girlswritenow", "softwareKeyboardLayoutMode": "pan", "adaptiveIcon": { "foregroundImage": "./assets/adaptive-icon.png", "backgroundColor": "#ffffff" - }, - "softwareKeyboardLayoutMode": "pan" + } }, "web": { "favicon": "./assets/favicon.png" }, - "plugins": ["expo-router"] + "plugins": ["expo-router"], + "extra": { + "router": { + "origin": false + }, + "eas": { + "projectId": "12e1580c-e57a-466e-bc0d-c7a051565998" + } + } } } diff --git a/package-lock.json b/package-lock.json index da49b463..4bd8250b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,9 @@ "@emotion/styled": "^11.11.0", "@expo-google-fonts/manrope": "^0.2.3", "@expo/vector-icons": "^13.0.0", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/react-native-fontawesome": "^0.3.0", "@mui/icons-material": "^5.14.13", "@mui/material": "^5.14.13", "@mui/styled-engine-sc": "^6.0.0-alpha.1", @@ -3656,6 +3659,53 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz", "integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==" }, + "node_modules/@fortawesome/fontawesome-common-types": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.2.tgz", + "integrity": "sha512-gBxPg3aVO6J0kpfHNILc+NMhXnqHumFxOmjYCFfOiLZfwhnnfhtsdA2hfJlDnj+8PjAs6kKQPenOTKj3Rf7zHw==", + "hasInstallScript": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/fontawesome-svg-core": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.2.tgz", + "integrity": "sha512-5CdaCBGl8Rh9ohNdxeeTMxIj8oc3KNBgIeLMvJosBMdslK/UnEB8rzyDRrbKdL1kDweqBPo4GT9wvnakHWucZw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/free-solid-svg-icons": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.2.tgz", + "integrity": "sha512-QWFZYXFE7O1Gr1dTIp+D6UcFUF0qElOnZptpi7PBUMylJh+vFmIedVe1Ir6RM1t2tEQLLSV1k7bR4o92M+uqlw==", + "hasInstallScript": true, + "dependencies": { + "@fortawesome/fontawesome-common-types": "6.5.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@fortawesome/react-native-fontawesome": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-native-fontawesome/-/react-native-fontawesome-0.3.0.tgz", + "integrity": "sha512-wSfetdK4+b/pvPbM2v+bZ5hfNlwtk9l3QuJo59sbMrxJalfX7BuF2WsSIWMSxfWwSsbOtY4+TUs6uw/rE59NJA==", + "dependencies": { + "humps": "^2.0.1", + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@fortawesome/fontawesome-svg-core": "~1 || ~6", + "react-native": ">= 0.67", + "react-native-svg": ">= 11.x" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -12522,6 +12572,11 @@ "node": ">=10.17.0" } }, + "node_modules/humps": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", + "integrity": "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g==" + }, "node_modules/husky": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/husky/-/husky-8.0.3.tgz", diff --git a/package.json b/package.json index 45a2c09d..d058a0c8 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "@emotion/styled": "^11.11.0", "@expo-google-fonts/manrope": "^0.2.3", "@expo/vector-icons": "^13.0.0", + "@fortawesome/fontawesome-svg-core": "^6.5.2", + "@fortawesome/free-solid-svg-icons": "^6.5.2", + "@fortawesome/react-native-fontawesome": "^0.3.0", "@mui/icons-material": "^5.14.13", "@mui/material": "^5.14.13", "@mui/styled-engine-sc": "^6.0.0-alpha.1", diff --git a/src/app/(tabs)/genre/_layout.tsx b/src/app/(tabs)/genre/_layout.tsx index d021a461..d309c307 100644 --- a/src/app/(tabs)/genre/_layout.tsx +++ b/src/app/(tabs)/genre/_layout.tsx @@ -1,10 +1,14 @@ import { Stack } from 'expo-router'; +import { FilterContextProvider } from '../../../utils/FilterContext'; + function StackLayout() { return ( - - - + + + + + ); } diff --git a/src/app/(tabs)/genre/index.tsx b/src/app/(tabs)/genre/index.tsx index 04483ea9..5bf00f55 100644 --- a/src/app/(tabs)/genre/index.tsx +++ b/src/app/(tabs)/genre/index.tsx @@ -7,13 +7,14 @@ import { Text, FlatList, } from 'react-native'; -import { MultiSelect } from 'react-native-element-dropdown'; +import { Dropdown, MultiSelect } from 'react-native-element-dropdown'; import { Icon } from 'react-native-elements'; import { TouchableOpacity } from 'react-native-gesture-handler'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; import BackButton from '../../../components/BackButton/BackButton'; + import PreviewCard from '../../../components/PreviewCard/PreviewCard'; import { fetchGenreStoryById } from '../../../queries/genres'; import { fetchStoryPreviewByIds } from '../../../queries/stories'; diff --git a/src/app/(tabs)/genre/styles.tsx b/src/app/(tabs)/genre/styles.tsx index 8d69590b..181292de 100644 --- a/src/app/(tabs)/genre/styles.tsx +++ b/src/app/(tabs)/genre/styles.tsx @@ -1,6 +1,7 @@ import { StyleSheet } from 'react-native'; import colors from '../../../styles/colors'; +import globalStyles from '../../../styles/globalStyles'; const styles = StyleSheet.create({ textSelected: { @@ -12,7 +13,6 @@ const styles = StyleSheet.create({ width: '100%', flex: 1, }, - flatListStyle: { paddingTop: 15, }, diff --git a/src/app/(tabs)/home/index.tsx b/src/app/(tabs)/home/index.tsx index 911f8a57..2c8c0e3e 100644 --- a/src/app/(tabs)/home/index.tsx +++ b/src/app/(tabs)/home/index.tsx @@ -7,6 +7,7 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; import ContentCard from '../../../components/ContentCard/ContentCard'; import PreviewCard from '../../../components/PreviewCard/PreviewCard'; + import { fetchUsername } from '../../../queries/profiles'; import { fetchFeaturedStoriesDescription, diff --git a/src/app/(tabs)/search/index.tsx b/src/app/(tabs)/search/index.tsx index 6163689c..10496c53 100644 --- a/src/app/(tabs)/search/index.tsx +++ b/src/app/(tabs)/search/index.tsx @@ -230,14 +230,14 @@ function SearchScreen() { }} /> - {search && ( - - + + + )} diff --git a/src/app/(tabs)/story/styles.ts b/src/app/(tabs)/story/styles.ts index 0a2dc686..9dd547c2 100644 --- a/src/app/(tabs)/story/styles.ts +++ b/src/app/(tabs)/story/styles.ts @@ -57,6 +57,17 @@ const styles = StyleSheet.create({ process: { marginBottom: 16, }, + backToTopButtonText: { + fontFamily: 'Manrope-Regular', + fontSize: 12, + fontWeight: '800', + textAlign: 'left', + color: 'black', + }, + bottomReactionContainer: { + flex: 1, + justifyContent: 'space-around', + }, }); export default styles; diff --git a/src/components/ContentCard/ContentCard.tsx b/src/components/ContentCard/ContentCard.tsx index b3c1d35f..181d0b9a 100644 --- a/src/components/ContentCard/ContentCard.tsx +++ b/src/components/ContentCard/ContentCard.tsx @@ -39,7 +39,7 @@ function ContentCard({ (async () => { const temp = await fetchAllReactionsToStory(id); if (temp != null) { - setReactions(temp.map(r => r.reaction)); + setReactions(temp); return; } @@ -76,8 +76,8 @@ function ContentCard({ - - + + diff --git a/src/components/FilterModal/ChildFilter.tsx b/src/components/FilterModal/ChildFilter.tsx new file mode 100644 index 00000000..3b93abd1 --- /dev/null +++ b/src/components/FilterModal/ChildFilter.tsx @@ -0,0 +1,27 @@ +import { CheckBox } from '@rneui/themed'; +import { memo } from 'react'; + +type ChildFilterProps = { + id: number; + name: string; + checked: boolean; + onPress: (id: number) => void; +}; + +function ChildFilter({ id, name, checked, onPress }: ChildFilterProps) { + return ( + onPress(id)} + iconType="material-community" + checkedIcon="checkbox-marked" + uncheckedIcon="checkbox-blank-outline" + checkedColor="black" + /> + ); +} + +export default memo(ChildFilter); diff --git a/src/components/FilterModal/FilterModal.tsx b/src/components/FilterModal/FilterModal.tsx index c2ded653..16fc7263 100644 --- a/src/components/FilterModal/FilterModal.tsx +++ b/src/components/FilterModal/FilterModal.tsx @@ -1,11 +1,14 @@ import { BottomSheet, CheckBox } from '@rneui/themed'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { View, Text, ScrollView, Pressable } from 'react-native'; +import { FlatList } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; -import 'react-native-gesture-handler'; +import ChildFilter from './ChildFilter'; +import ParentFilter from './ParentFilter'; import styles from './styles'; import Icon from '../../../assets/icons'; +import { TagFilter, useFilter } from '../../utils/FilterContext'; type FilterModalProps = { isVisible: boolean; @@ -13,33 +16,32 @@ type FilterModalProps = { title: string; }; +export enum CATEGORIES { + GENRE = 'genre-medium', + TOPIC = 'topic', + TONE = 'tone', +} + function FilterModal({ isVisible, setIsVisible, title }: FilterModalProps) { - const [checked1, toggleChecked1] = useState(false); - const [checked2, toggleChecked2] = useState(false); - const [checked3, toggleChecked3] = useState(false); + const { dispatch, filters } = useFilter(); - const genres = [ - { - title: 'Fiction', - state: checked1, - setState: toggleChecked1, + const toggleParentFilter = useCallback( + (id: number) => { + dispatch({ type: 'TOGGLE_MAIN_GENRE', mainGenreId: id }); }, - { - title: 'Erasure & Found Poetry', - state: checked2, - setState: toggleChecked2, - }, - { - title: 'Non-Fiction', - state: checked3, - setState: toggleChecked3, + [dispatch], + ); + + const toggleChildFilter = useCallback( + (id: number) => { + dispatch({ type: 'TOGGLE_FILTER', id }); }, - ]; + [dispatch], + ); return ( {title} - - {genres.map(item => { + { + const [_, parentFilter] = item; return ( - item.setState(!item.state)} - iconType="material-community" - checkedIcon="checkbox-marked" - uncheckedIcon="checkbox-blank-outline" - checkedColor="black" - /> + <> + + + { + return ( + + ); + }} + /> + ); - })} - + }} + /> diff --git a/src/components/FilterModal/ParentFilter.tsx b/src/components/FilterModal/ParentFilter.tsx new file mode 100644 index 00000000..d763e0a5 --- /dev/null +++ b/src/components/FilterModal/ParentFilter.tsx @@ -0,0 +1,26 @@ +import { CheckBox } from '@rneui/themed'; +import { memo } from 'react'; + +type ParentFilterProps = { + id: number; + name: string; + checked: boolean; + onPress: (id: number) => void; +}; + +function ParentFilter({ id, name, checked, onPress }: ParentFilterProps) { + return ( + onPress(id)} + iconType="material-community" + checkedIcon="checkbox-marked" + uncheckedIcon="checkbox-blank-outline" + checkedColor="black" + /> + ); +} + +export default memo(ParentFilter); diff --git a/src/components/PreviewCard/PreviewCard.tsx b/src/components/PreviewCard/PreviewCard.tsx index 71f3589b..53164d4e 100644 --- a/src/components/PreviewCard/PreviewCard.tsx +++ b/src/components/PreviewCard/PreviewCard.tsx @@ -1,6 +1,6 @@ import * as cheerio from 'cheerio'; import { Image } from 'expo-image'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { GestureResponderEvent, Pressable, @@ -24,7 +24,7 @@ type PreviewCardProps = { storyId: number; author: string; authorImage: string; - defaultSavedStoriesState?: boolean; + defaultSavedStoriesState?: boolean | null; excerpt: { html: string }; tags: string[]; reactions?: string[] | null; @@ -39,7 +39,7 @@ function PreviewCard({ authorImage, excerpt, tags, - defaultSavedStoriesState = false, + defaultSavedStoriesState = null, pressFunction, reactions: preloadedReactions = null, }: PreviewCardProps) { @@ -54,7 +54,7 @@ function PreviewCard({ (async () => { const temp = await fetchAllReactionsToStory(storyId); if (temp != null) { - setReactions(temp.map(r => r.reaction)); + setReactions(temp.filter(r => r != null)); return; } setReactions([]); @@ -101,7 +101,7 @@ function PreviewCard({ - + {(tags?.length ?? 0) > 0 && ( diff --git a/src/components/ReactionDisplay/ReactionDisplay.tsx b/src/components/ReactionDisplay/ReactionDisplay.tsx index 6b70eb1e..22f490b0 100644 --- a/src/components/ReactionDisplay/ReactionDisplay.tsx +++ b/src/components/ReactionDisplay/ReactionDisplay.tsx @@ -2,20 +2,27 @@ import { Text, View } from 'react-native'; import styles from './styles'; import Emoji from 'react-native-emoji'; import globalStyles from '../../styles/globalStyles'; +import { Channel, usePubSub } from '../../utils/PubSubContext'; +import { useEffect, useState } from 'react'; type ReactionDisplayProps = { reactions: (string | null)[]; + storyId: number; }; -function ReactionDisplay({ reactions }: ReactionDisplayProps) { - const cleanedReactions = reactions.filter(reaction => reaction !== null); - const reactionColors: Record = { - heart: '#FFCCCB', - clap: '#FFD580', - cry: '#89CFF0', - hugging_face: '#ffc3bf', - muscle: '#eddcf7', - }; +const reactionColors: Record = { + heart: '#FFCCCB', + clap: '#FFD580', + cry: '#89CFF0', + hugging_face: '#ffc3bf', + muscle: '#eddcf7', +}; + +function ReactionDisplay({ reactions, storyId }: ReactionDisplayProps) { + const { channels, getPubSubValue } = usePubSub(); + const [reactionCount, setReactionCount] = useState(0); + + const cleanedReactions = reactions.filter(reaction => reaction != null); const defaultColor = reactionColors['heart']; const setOfReactions = [...cleanedReactions]; setOfReactions.push('heart'); @@ -23,6 +30,24 @@ function ReactionDisplay({ reactions }: ReactionDisplayProps) { setOfReactions.push('muscle'); const reactionDisplay = [...new Set(setOfReactions)].slice(0, 3); + const serverReactionCount = cleanedReactions?.length ?? 0; + + useEffect(() => { + setReactionCount(serverReactionCount); + }, [reactions]); + + useEffect(() => { + const value = getPubSubValue(Channel.REACTIONS, storyId); + if (value == undefined) { + return; + } + + if (value) { + setReactionCount(serverReactionCount + 1); + } else { + setReactionCount(serverReactionCount); + } + }, [channels[Channel.REACTIONS][storyId]]); return ( {reactionDisplay.map(reaction => { - if (reaction === null) return; + if (reaction == null) return; return ( - {cleanedReactions?.length ?? 0} + {reactionCount} diff --git a/src/components/ReactionPicker/ReactionPicker.tsx b/src/components/ReactionPicker/ReactionPicker.tsx new file mode 100644 index 00000000..637a402e --- /dev/null +++ b/src/components/ReactionPicker/ReactionPicker.tsx @@ -0,0 +1,81 @@ +import { faFaceSmile } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; +import { useEffect, useState } from 'react'; +import { View, TouchableOpacity } from 'react-native'; + +import styles from './styles'; +import Emoji from 'react-native-emoji'; +import { + addReactionToStory, + deleteReactionToStory, +} from '../../queries/reactions'; +import { useSession } from '../../utils/AuthContext'; +import { Channel, usePubSub } from '../../utils/PubSubContext'; + +type ReactionPickerProps = { + storyId: number; +}; + +const ReactionPicker = ({ storyId }: ReactionPickerProps) => { + const { user } = useSession(); + const { publish } = usePubSub(); + const [showReactions, setShowReactions] = useState(false); + const [currentReaction, setCurrentReaction] = useState(''); + + const toggleReactions = () => setShowReactions(!showReactions); + const reactionMapping: Record = { + heart: 2, + clap: 3, + muscle: 4, + cry: 5, + hugging_face: 6, + }; + + const handleReactionPress = (reactionName: string) => { + if (currentReaction == reactionName) { + removeReaction(reactionName); + } else { + addReaction(reactionName); + } + }; + + const addReaction = (reactionName: string) => { + setCurrentReaction(reactionName); + publish(Channel.REACTIONS, storyId, true); + + const reactionId = reactionMapping[reactionName]; + addReactionToStory(user?.id, storyId, reactionId); + }; + + const removeReaction = (reactionName: string) => { + setCurrentReaction(''); + publish(Channel.REACTIONS, storyId, false); + + const reactionId = reactionMapping[reactionName]; + deleteReactionToStory(user?.id, storyId, reactionId); + }; + + return ( + + + + + {showReactions && ( + <> + + {Object.keys(reactionMapping).map((reaction, i) => ( + handleReactionPress(reaction)} + > + + + ))} + + )} + + + ); +}; + +export default ReactionPicker; diff --git a/src/components/ReactionPicker/styles.ts b/src/components/ReactionPicker/styles.ts new file mode 100644 index 00000000..5a5a7052 --- /dev/null +++ b/src/components/ReactionPicker/styles.ts @@ -0,0 +1,29 @@ +import { StyleSheet } from 'react-native'; + +import colors from '../../styles/colors'; + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'flex-end', + // flexDirection: 'row' + }, + reactionView: { + borderRadius: 20, + padding: 10, + alignSelf: 'center', + marginBottom: 10, + }, + reactionsContainer: { + flexDirection: 'row', + gap: 5, + justifyContent: 'space-between', + padding: 10, + position: 'absolute', // Positioning the container above the toggle button + bottom: 50, + backgroundColor: '#D9D9D9', + borderRadius: 20, + }, +}); + +export default styles; diff --git a/src/components/SaveStoryButton/SaveStoryButton.tsx b/src/components/SaveStoryButton/SaveStoryButton.tsx index be44ebb9..307d1d7e 100644 --- a/src/components/SaveStoryButton/SaveStoryButton.tsx +++ b/src/components/SaveStoryButton/SaveStoryButton.tsx @@ -4,7 +4,7 @@ import { deleteUserStoryToReadingList, isStoryInReadingList, } from '../../queries/savedStories'; -import { usePubSub } from '../../utils/PubSubContext'; +import { Channel, usePubSub } from '../../utils/PubSubContext'; import { useSession } from '../../utils/AuthContext'; import { Image } from 'expo-image'; import { TouchableOpacity } from 'react-native-gesture-handler'; @@ -25,7 +25,7 @@ export default function SaveStoryButton({ const [storyIsSaved, setStoryIsSaved] = useState( defaultState, ); - const { channels, initializeChannel, publish } = usePubSub(); + const { publish, channels, getPubSubValue } = usePubSub(); useEffect(() => { if (defaultState != null) { @@ -34,20 +34,18 @@ export default function SaveStoryButton({ isStoryInReadingList(storyId, user?.id).then(storyInReadingList => { setStoryIsSaved(storyInReadingList); - initializeChannel(storyId); }); }, [storyId]); useEffect(() => { - // if another card updates this story, update it here also - if (typeof channels[storyId] !== 'undefined') { - setStoryIsSaved(channels[storyId] ?? false); + if (getPubSubValue(Channel.SAVED_STORIES, storyId) != null) { + setStoryIsSaved(getPubSubValue(Channel.SAVED_STORIES, storyId) ?? false); } - }, [channels[storyId]]); + }, [channels[Channel.SAVED_STORIES][storyId]]); const saveStory = async (saved: boolean) => { setStoryIsSaved(saved); - publish(storyId, saved); // update other cards with this story + publish(Channel.SAVED_STORIES, storyId, saved); // update other cards with this story if (saved) { await addUserStoryToReadingList(user?.id, storyId); diff --git a/src/queries/reactions.tsx b/src/queries/reactions.tsx index ffe68c40..f72c9e00 100644 --- a/src/queries/reactions.tsx +++ b/src/queries/reactions.tsx @@ -2,7 +2,7 @@ import { Reactions } from './types'; import supabase from '../utils/supabase'; export async function addReactionToStory( - input_profile_id: number, + input_profile_id: string | undefined, input_story_id: number, input_reaction_id: number, ): Promise { @@ -22,7 +22,7 @@ export async function addReactionToStory( } export async function deleteReactionToStory( - input_profile_id: number, + input_profile_id: string | undefined, input_story_id: number, input_reaction_id: number, ): Promise { @@ -43,17 +43,18 @@ export async function deleteReactionToStory( export async function fetchAllReactionsToStory( storyId: number, -): Promise { +): Promise { const { data, error } = await supabase.rpc('curr_get_reactions_for_story', { - input_story_id: storyId, + story_id: storyId, }); + if (error) { console.log(error); throw new Error( `An error occured when trying to fetch reactions to a story', ${error}`, ); } else { - return data as Reactions[]; + return data[0].reactions; } } diff --git a/src/utils/PubSubContext.tsx b/src/utils/PubSubContext.tsx index fbb9ada6..9079389f 100644 --- a/src/utils/PubSubContext.tsx +++ b/src/utils/PubSubContext.tsx @@ -1,9 +1,17 @@ -import React, { createContext, useContext, useMemo, useState } from 'react'; +import React, { createContext, useContext, useState } from 'react'; + +export enum Channel { + REACTIONS = 'reactions', + SAVED_STORIES = 'saved_stories', + FAVORITES = 'favorites', +} + +type channel = Record; export interface PubSubState { - channels: Record; - initializeChannel: (id: number) => void; - publish: (id: number, message: boolean) => void; + channels: Record; + publish: (channel: Channel, id: number, message: boolean) => void; + getPubSubValue: (channel: Channel, id: number) => boolean | undefined; } const BooleanPubSubContext = createContext({} as PubSubState); @@ -26,28 +34,26 @@ export function BooleanPubSubProvider({ }: { children: React.ReactNode; }) { - const [channels, setChannels] = useState>( - {}, - ); - - const initializeChannel = (id: number) => { - if (!(id in channels)) { - setChannels({ ...channels, [id]: undefined }); - } + const [channels, setChannels] = useState>({ + [Channel.FAVORITES]: {}, + [Channel.REACTIONS]: {}, + [Channel.SAVED_STORIES]: {}, + }); + + const publish = (channel: Channel, id: number, message: boolean) => { + let thisChannel = { ...channels[channel], [id]: message }; + setChannels({ ...channels, [channel]: thisChannel }); }; - const publish = (id: number, message: boolean) => { - setChannels({ ...channels, [id]: message }); + const getPubSubValue = (channel: Channel, id: number) => { + return channels[channel][id]; }; - const authContextValue = useMemo( - () => ({ - channels, - initializeChannel, - publish, - }), - [channels], - ); + const authContextValue = { + channels, + publish, + getPubSubValue, + }; return (