From 70b0844acdb1c93bfa95e6ce17911cafffd19da8 Mon Sep 17 00:00:00 2001 From: Marcoss28 <69034384+Marcoss28@users.noreply.github.com> Date: Sun, 21 Apr 2024 14:30:32 -0700 Subject: [PATCH] [react] Fetch reactions to story for content card (#87) * accidently forgot to branch * mergong * reactions added to the PreviewCard and ContentCard * prettier problems * minor change * fixed small error * Improve performence * Preload reactions * Clean up code * Make content card use string[] * Add null checks for preview length * Add reactions display component * Run prettier --------- Co-authored-by: Marcos Hernandez Co-authored-by: Aditya Pawar Co-authored-by: Aditya Pawar <34043950+adityapawar1@users.noreply.github.com> --- src/app/(tabs)/author/index.tsx | 4 +- src/app/(tabs)/home/index.tsx | 2 + src/app/(tabs)/search/index.tsx | 22 ++++--- src/app/(tabs)/story/index.tsx | 2 +- src/app/auth/verify/index.tsx | 5 +- src/components/AuthorCard/AuthorCard.tsx | 3 +- src/components/ContentCard/ContentCard.tsx | 42 +++++++------ .../GenreStoryPreviewCard.tsx | 2 +- src/components/PreviewCard/PreviewCard.tsx | 46 +++++++------- .../ReactionDisplay/ReactionDisplay.tsx | 60 +++++++++++++++++++ src/components/ReactionDisplay/styles.tsx | 27 +++++++++ .../SaveStoryButton/SaveStoryButton.tsx | 20 ++++--- src/components/SplashScreen/SplashScreen.tsx | 3 +- src/queries/stories.tsx | 13 +++- src/queries/types.tsx | 20 +++++-- src/utils/FilterContext.tsx | 1 + tsconfig.json | 1 + 17 files changed, 202 insertions(+), 71 deletions(-) create mode 100644 src/components/ReactionDisplay/ReactionDisplay.tsx create mode 100644 src/components/ReactionDisplay/styles.tsx diff --git a/src/app/(tabs)/author/index.tsx b/src/app/(tabs)/author/index.tsx index 542738f8..1feee80a 100644 --- a/src/app/(tabs)/author/index.tsx +++ b/src/app/(tabs)/author/index.tsx @@ -1,8 +1,8 @@ import * as cheerio from 'cheerio'; import { useLocalSearchParams, router } from 'expo-router'; -import { decode } from 'html-entities'; import { useEffect, useState } from 'react'; -import { ActivityIndicator, ScrollView, View, Text, Image } from 'react-native'; +import { ActivityIndicator, ScrollView, View, Text } from 'react-native'; +import { Image } from 'expo-image'; import { SafeAreaView } from 'react-native-safe-area-context'; import styles from './styles'; diff --git a/src/app/(tabs)/home/index.tsx b/src/app/(tabs)/home/index.tsx index b5c7b343..2b262871 100644 --- a/src/app/(tabs)/home/index.tsx +++ b/src/app/(tabs)/home/index.tsx @@ -129,6 +129,7 @@ function HomeScreen() { > {recommendedStories.map(story => ( {newStories.map(story => ( { }; function SearchScreen() { - const [allStories, setAllStories] = useState([]); + const [allStories, setAllStories] = useState< + StoryPreviewWithPreloadedReactions[] + >([]); const [allGenres, setAllGenres] = useState([]); - const [searchResults, setSearchResults] = useState([]); + const [searchResults, setSearchResults] = useState< + StoryPreviewWithPreloadedReactions[] + >([]); const [search, setSearch] = useState(''); const [filterVisible, setFilterVisible] = useState(false); const [recentSearches, setRecentSearches] = useState([]); @@ -75,9 +84,7 @@ function SearchScreen() { useEffect(() => { (async () => { - fetchAllStoryPreviews().then((stories: StoryPreview[]) => - setAllStories(stories), - ); + fetchAllStoryPreviews().then(stories => setAllStories(stories)); fetchGenres().then((genres: Genre[]) => setAllGenres(genres)); getRecentSearch().then((searches: RecentSearch[]) => setRecentSearches(searches), @@ -99,7 +106,7 @@ function SearchScreen() { return; } - const updatedData = allStories.filter((item: StoryPreview) => { + const updatedData = allStories.filter(item => { const title = `${item.title.toUpperCase()})`; const author = `${item.author_name.toUpperCase()})`; const text_data = text.toUpperCase(); @@ -400,6 +407,7 @@ function SearchScreen() { storyId={item.id} title={item.title} image={item.featured_media} + reactions={item.reactions} author={item.author_name} authorImage={item.author_image} excerpt={item.excerpt} diff --git a/src/app/(tabs)/story/index.tsx b/src/app/(tabs)/story/index.tsx index 3581936b..50602db8 100644 --- a/src/app/(tabs)/story/index.tsx +++ b/src/app/(tabs)/story/index.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'; import { ActivityIndicator, FlatList, - Image, ScrollView, Share, Text, @@ -11,6 +10,7 @@ import { View, useWindowDimensions, } from 'react-native'; +import { Image } from 'expo-image'; import { Button } from 'react-native-paper'; import { RenderHTML } from 'react-native-render-html'; import { SafeAreaView } from 'react-native-safe-area-context'; diff --git a/src/app/auth/verify/index.tsx b/src/app/auth/verify/index.tsx index a9b128ae..a14cf766 100644 --- a/src/app/auth/verify/index.tsx +++ b/src/app/auth/verify/index.tsx @@ -1,10 +1,9 @@ -import { Link, Redirect, router, useLocalSearchParams } from 'expo-router'; +import { Redirect, router, useLocalSearchParams } from 'expo-router'; import { useState, useRef, useEffect } from 'react'; import { View, Text } from 'react-native'; -import { Icon } from 'react-native-elements'; import OTPTextInput from 'react-native-otp-textinput'; import { SafeAreaView } from 'react-native-safe-area-context'; -import Toast, { BaseToast, BaseToastProps } from 'react-native-toast-message'; +import Toast from 'react-native-toast-message'; import styles from './styles'; import BackButton from '../../../components/BackButton/BackButton'; diff --git a/src/components/AuthorCard/AuthorCard.tsx b/src/components/AuthorCard/AuthorCard.tsx index 99694abd..edec4725 100644 --- a/src/components/AuthorCard/AuthorCard.tsx +++ b/src/components/AuthorCard/AuthorCard.tsx @@ -1,4 +1,5 @@ -import { Image, Text, View } from 'react-native'; +import { Image } from 'expo-image'; +import { Text, View } from 'react-native'; import styles from './styles'; type AuthorCardProps = { diff --git a/src/components/ContentCard/ContentCard.tsx b/src/components/ContentCard/ContentCard.tsx index c766751a..c0ac44f7 100644 --- a/src/components/ContentCard/ContentCard.tsx +++ b/src/components/ContentCard/ContentCard.tsx @@ -1,3 +1,5 @@ +import { Image } from 'expo-image'; +import { useEffect, useState } from 'react'; import { GestureResponderEvent, Pressable, @@ -5,14 +7,17 @@ import { View, TouchableOpacity, } from 'react-native'; -import { Image } from 'expo-image'; +import Emoji from 'react-native-emoji'; import styles from './styles'; +import { fetchAllReactionsToStory } from '../../queries/reactions'; +import { Reactions } from '../../queries/types'; import globalStyles from '../../styles/globalStyles'; -import Emoji from 'react-native-emoji'; import SaveStoryButton from '../SaveStoryButton/SaveStoryButton'; +import ReactionDisplay from '../ReactionDisplay/ReactionDisplay'; type ContentCardProps = { + id: number; title: string; author: string; image: string; @@ -22,6 +27,7 @@ type ContentCardProps = { }; function ContentCard({ + id, title, author, image, @@ -29,6 +35,20 @@ function ContentCard({ storyId, pressFunction, }: ContentCardProps) { + const [reactions, setReactions] = useState(); + + useEffect(() => { + (async () => { + const temp = await fetchAllReactionsToStory(id); + if (temp != null) { + setReactions(temp.map(r => r.reaction)); + return; + } + + setReactions([]); + })(); + }, []); + return ( @@ -58,23 +78,7 @@ function ContentCard({ - - - - - - - - - - - {/* heart, clap, muscle, cry, ??? */} - - - 14{/*change number to work*/} - - - + diff --git a/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx b/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx index 00b413da..7ea1ba20 100644 --- a/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx +++ b/src/components/GenreStoryPreviewCard/GenreStoryPreviewCard.tsx @@ -1,10 +1,10 @@ import { GestureResponderEvent, Text, - Image, View, TouchableOpacity, } from 'react-native'; +import { Image } from 'expo-image'; import styles from './styles'; import globalStyles from '../../styles/globalStyles'; diff --git a/src/components/PreviewCard/PreviewCard.tsx b/src/components/PreviewCard/PreviewCard.tsx index a41e7ab5..f561cd48 100644 --- a/src/components/PreviewCard/PreviewCard.tsx +++ b/src/components/PreviewCard/PreviewCard.tsx @@ -1,4 +1,6 @@ import * as cheerio from 'cheerio'; +import { Image } from 'expo-image'; +import { useEffect, useState } from 'react'; import { GestureResponderEvent, Pressable, @@ -7,11 +9,13 @@ import { View, } from 'react-native'; import Emoji from 'react-native-emoji'; -import { Image } from 'expo-image'; import styles from './styles'; +import { fetchAllReactionsToStory } from '../../queries/reactions'; +import { Reactions } from '../../queries/types'; import globalStyles from '../../styles/globalStyles'; import SaveStoryButton from '../SaveStoryButton/SaveStoryButton'; +import ReactionDisplay from '../ReactionDisplay/ReactionDisplay'; const placeholderImage = 'https://gwn-uploads.s3.amazonaws.com/wp-content/uploads/2021/10/10120952/Girls-Write-Now-logo-avatar.png'; @@ -24,6 +28,7 @@ type PreviewCardProps = { authorImage: string; excerpt: { html: string }; tags: string[]; + reactions?: string[] | null; pressFunction: (event: GestureResponderEvent) => void; }; @@ -36,10 +41,25 @@ function PreviewCard({ excerpt, tags, pressFunction, + reactions: preloadedReactions = null, }: PreviewCardProps) { - const saveStory = () => { - console.log("testing '+' icon does something for story " + title); - }; + const [reactions, setReactions] = useState( + preloadedReactions, + ); + useEffect(() => { + if (preloadedReactions != null) { + return; + } + + (async () => { + const temp = await fetchAllReactionsToStory(storyId); + if (temp != null) { + setReactions(temp.map(r => r.reaction)); + return; + } + setReactions([]); + })(); + }, []); return ( @@ -76,23 +96,7 @@ function PreviewCard({ - - - - - - - - - - - {/* heart, clap, muscle, cry, ??? */} - - - 14{/*change number to work*/} - - - + {(tags?.length ?? 0) > 0 && ( diff --git a/src/components/ReactionDisplay/ReactionDisplay.tsx b/src/components/ReactionDisplay/ReactionDisplay.tsx new file mode 100644 index 00000000..0bfb0970 --- /dev/null +++ b/src/components/ReactionDisplay/ReactionDisplay.tsx @@ -0,0 +1,60 @@ +import { Text, View } from 'react-native'; +import styles from './styles'; +import Emoji from 'react-native-emoji'; +import globalStyles from '../../styles/globalStyles'; + +type ReactionDisplayProps = { + reactions: string[]; +}; + +function ReactionDisplay({ reactions }: ReactionDisplayProps) { + const reactionColors: Record = { + heart: '#FFCCCB', + clap: '#FFD580', + cry: '#89CFF0', + hugging_face: '#ffc3bf', + muscle: '#eddcf7', + }; + const defaultColor = reactionColors['heart']; + const setOfReactions = [...reactions]; + setOfReactions.push('heart'); + setOfReactions.push('clap'); + setOfReactions.push('muscle'); + + const reactionDisplay = [...new Set(setOfReactions)].slice(0, 3); + + return ( + + {reactionDisplay.map(reaction => { + return ( + + + + ); + })} + + + {reactions?.length ?? 0} + + + + ); +} + +export default ReactionDisplay; diff --git a/src/components/ReactionDisplay/styles.tsx b/src/components/ReactionDisplay/styles.tsx new file mode 100644 index 00000000..e0d37ab4 --- /dev/null +++ b/src/components/ReactionDisplay/styles.tsx @@ -0,0 +1,27 @@ +import { StyleSheet } from 'react-native'; +import colors from '../../styles/colors'; + +const styles = StyleSheet.create({ + reactions: { + width: 32, + height: 32, + borderRadius: 32 / 2, + borderWidth: 1, + backgroundColor: '#89CFF0', //different per emoji reaction + borderColor: 'white', + marginTop: 10, + marginRight: -5, // -10 + overflow: 'hidden', + justifyContent: 'center', + paddingLeft: 4, + }, + reactionText: { + color: colors.grey, + }, + reactionNumber: { + marginLeft: 16, + marginTop: 16, + }, +}); + +export default styles; diff --git a/src/components/SaveStoryButton/SaveStoryButton.tsx b/src/components/SaveStoryButton/SaveStoryButton.tsx index c4d43315..be44ebb9 100644 --- a/src/components/SaveStoryButton/SaveStoryButton.tsx +++ b/src/components/SaveStoryButton/SaveStoryButton.tsx @@ -11,17 +11,27 @@ import { TouchableOpacity } from 'react-native-gesture-handler'; type SaveStoryButtonProps = { storyId: number; + defaultState?: boolean | null; }; const saveStoryImage = require('../../../assets/save_story.png'); const savedStoryImage = require('../../../assets/saved_story.png'); -export default function SaveStoryButton({ storyId }: SaveStoryButtonProps) { +export default function SaveStoryButton({ + storyId, + defaultState = null, +}: SaveStoryButtonProps) { const { user } = useSession(); - const [storyIsSaved, setStoryIsSaved] = useState(false); + const [storyIsSaved, setStoryIsSaved] = useState( + defaultState, + ); const { channels, initializeChannel, publish } = usePubSub(); useEffect(() => { + if (defaultState != null) { + return; + } + isStoryInReadingList(storyId, user?.id).then(storyInReadingList => { setStoryIsSaved(storyInReadingList); initializeChannel(storyId); @@ -35,12 +45,6 @@ export default function SaveStoryButton({ storyId }: SaveStoryButtonProps) { } }, [channels[storyId]]); - useEffect(() => { - isStoryInReadingList(storyId, user?.id).then(storyInReadingList => - setStoryIsSaved(storyInReadingList), - ); - }, [storyId]); - const saveStory = async (saved: boolean) => { setStoryIsSaved(saved); publish(storyId, saved); // update other cards with this story diff --git a/src/components/SplashScreen/SplashScreen.tsx b/src/components/SplashScreen/SplashScreen.tsx index 3d791035..d9585ce9 100644 --- a/src/components/SplashScreen/SplashScreen.tsx +++ b/src/components/SplashScreen/SplashScreen.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import { View, Image } from 'react-native'; +import { View } from 'react-native'; +import { Image } from 'expo-image'; import styles from './styles'; diff --git a/src/queries/stories.tsx b/src/queries/stories.tsx index 9b8e833b..e48dc51b 100644 --- a/src/queries/stories.tsx +++ b/src/queries/stories.tsx @@ -1,7 +1,14 @@ -import { Story, StoryPreview, StoryCard } from './types'; +import { + Story, + StoryPreview, + StoryCard, + StoryPreviewWithPreloadedReactions, +} from './types'; import supabase from '../utils/supabase'; -export async function fetchAllStoryPreviews(): Promise { +export async function fetchAllStoryPreviews(): Promise< + StoryPreviewWithPreloadedReactions[] +> { const { data, error } = await supabase.rpc('fetch_all_story_previews'); if (error) { @@ -25,7 +32,7 @@ export async function fetchStory(storyId: number): Promise { `An error occured when trying to fetch story ${storyId}: ${error.code}`, ); } else { - return data; + return data as Story[]; } } diff --git a/src/queries/types.tsx b/src/queries/types.tsx index 60748adc..a7ce63a0 100644 --- a/src/queries/types.tsx +++ b/src/queries/types.tsx @@ -11,6 +11,20 @@ export interface StoryPreview { genre_medium: string[]; } +export interface StoryPreviewWithPreloadedReactions { + id: number; + date: string; + title: string; + excerpt: { html: string }; + featured_media: string; + author_name: string; + author_image: string; + topic: string[]; + tone: string[]; + genre_medium: string[]; + reactions: string[]; +} + export interface Author { id: number; name: string; @@ -70,8 +84,6 @@ export interface GenreStories { } export interface Reactions { - profile_id: number; - story_id: number; - emoji_id: number; - emoji: string; + reaction_id: number; + reaction: string; } diff --git a/src/utils/FilterContext.tsx b/src/utils/FilterContext.tsx index b488e0dd..fa999b8d 100644 --- a/src/utils/FilterContext.tsx +++ b/src/utils/FilterContext.tsx @@ -5,6 +5,7 @@ import React, { useMemo, useReducer, } from 'react'; + import supabase from './supabase'; type FilterAction = diff --git a/tsconfig.json b/tsconfig.json index a0863d0f..e55b0e42 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "extends": "expo/tsconfig.base", "compilerOptions": { + "module": "esnext", "strict": true, "jsx": "react-jsx" }