diff --git a/package-lock.json b/package-lock.json
index f83fb7da..68298471 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -33,6 +33,7 @@
"expo": "~49.0.11",
"expo-constants": "~14.4.2",
"expo-font": "~11.4.0",
+ "expo-image": "~1.3.5",
"expo-linking": "~5.0.2",
"expo-router": "^2.0.0",
"expo-status-bar": "~1.6.0",
@@ -11174,6 +11175,14 @@
"react-native": "*"
}
},
+ "node_modules/expo-image": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/expo-image/-/expo-image-1.3.5.tgz",
+ "integrity": "sha512-yrIR2mnfIKbKcguoqWK3U5m3zvLPnonvSCabB2ErVGhws8zQs7ILYf+7T08j8U6eFcohjw0CoAFJ6RWNsX2EhA==",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-keep-awake": {
"version": "12.3.0",
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-12.3.0.tgz",
diff --git a/package.json b/package.json
index ed0b39df..d15d3b50 100644
--- a/package.json
+++ b/package.json
@@ -68,7 +68,8 @@
"react-native-vector-icons": "^10.0.2",
"react-scroll-to-top": "^3.0.0",
"use-debounce": "^10.0.0",
- "validator": "^13.11.0"
+ "validator": "^13.11.0",
+ "expo-image": "~1.3.5"
},
"devDependencies": {
"@babel/core": "^7.20.0",
diff --git a/src/app/_layout.tsx b/src/app/_layout.tsx
index c15ba80c..b8f05279 100644
--- a/src/app/_layout.tsx
+++ b/src/app/_layout.tsx
@@ -2,6 +2,7 @@ import { Stack } from 'expo-router';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { AuthContextProvider } from '../utils/AuthContext';
+import { BooleanPubSubProvider } from '../utils/PubSubContext';
import ToastComponent from '../components/Toast/Toast';
import { Keyboard, TouchableWithoutFeedback } from 'react-native';
@@ -9,11 +10,13 @@ function StackLayout() {
return (
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/components/AuthorCard/AuthorCard.tsx b/src/components/AuthorCard/AuthorCard.tsx
index afdd2b57..99694abd 100644
--- a/src/components/AuthorCard/AuthorCard.tsx
+++ b/src/components/AuthorCard/AuthorCard.tsx
@@ -1,7 +1,5 @@
-import { Image, Pressable, Text, View } from 'react-native';
-
+import { Image, Text, View } from 'react-native';
import styles from './styles';
-import globalStyles from '../../styles/globalStyles';
type AuthorCardProps = {
name: string;
diff --git a/src/components/ContentCard/ContentCard.tsx b/src/components/ContentCard/ContentCard.tsx
index a6e32b61..034ff6fa 100644
--- a/src/components/ContentCard/ContentCard.tsx
+++ b/src/components/ContentCard/ContentCard.tsx
@@ -1,19 +1,23 @@
import {
GestureResponderEvent,
- Image,
Pressable,
Text,
View,
TouchableOpacity,
} from 'react-native';
+import { Image } from 'expo-image';
import styles from './styles';
-import { addUserStoryToReadingList, deleteUserStoryToReadingList, isStoryInReadingList } from '../../queries/savedStories';
+import {
+ addUserStoryToReadingList,
+ deleteUserStoryToReadingList,
+ isStoryInReadingList,
+} from '../../queries/savedStories';
import globalStyles from '../../styles/globalStyles';
import { useSession } from '../../utils/AuthContext';
import Emoji from 'react-native-emoji';
-import { useEffect, useMemo, useState } from 'react';
-
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import { usePubSub } from '../../utils/PubSubContext';
type ContentCardProps = {
title: string;
@@ -37,19 +41,47 @@ function ContentCard({
}: ContentCardProps) {
const { user } = useSession();
const [storyIsSaved, setStoryIsSaved] = useState(false);
+ const { channels, initializeChannel, publish } = usePubSub();
+
+ const savedStoryImageComponent = useMemo(() => {
+ return (
+
+ )
+ }, [])
+ const saveStoryImageComponent = useMemo(() => {
+ return (
+
+ )
+ }, [])
useEffect(() => {
- isStoryInReadingList(storyId, user?.id).then(storyInReadingList => setStoryIsSaved(storyInReadingList))
- }, [storyId])
+ 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]);
+ }
+ }, [channels[storyId]]);
- const saveStory = () => {
- if (storyIsSaved) {
- deleteUserStoryToReadingList(user?.id, storyId);
+ const saveStory = async (saved: boolean) => {
+ setStoryIsSaved(saved);
+ publish(storyId, saved);
+ if (saved) {
+ await addUserStoryToReadingList(user?.id, storyId);
} else {
- addUserStoryToReadingList(user?.id, storyId);
+ await deleteUserStoryToReadingList(user?.id, storyId);
}
- setStoryIsSaved(!storyIsSaved);
};
return (
@@ -98,11 +130,10 @@ function ContentCard({
- saveStory()}>
-
+ saveStory(!storyIsSaved)}>
+ {storyIsSaved ?
+ savedStoryImageComponent : saveStoryImageComponent
+ }
diff --git a/src/components/PreviewCard/PreviewCard.tsx b/src/components/PreviewCard/PreviewCard.tsx
index db5b283f..fcb7b0b2 100644
--- a/src/components/PreviewCard/PreviewCard.tsx
+++ b/src/components/PreviewCard/PreviewCard.tsx
@@ -1,20 +1,25 @@
import * as cheerio from 'cheerio';
import {
GestureResponderEvent,
- Image,
Pressable,
Text,
TouchableOpacity,
View,
} from 'react-native';
import Emoji from 'react-native-emoji';
+import { Image } from 'expo-image';
import styles from './styles';
import globalStyles from '../../styles/globalStyles';
-import { useEffect, useState } from 'react';
-import { addUserStoryToReadingList, deleteUserStoryToReadingList, isStoryInReadingList } from '../../queries/savedStories';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+import {
+ addUserStoryToReadingList,
+ deleteUserStoryToReadingList,
+ isStoryInReadingList,
+} from '../../queries/savedStories';
import { useSession } from '../../utils/AuthContext';
import { useIsFocused } from '@react-navigation/native';
+import { usePubSub } from '../../utils/PubSubContext';
const placeholderImage =
'https://gwn-uploads.s3.amazonaws.com/wp-content/uploads/2021/10/10120952/Girls-Write-Now-logo-avatar.png';
@@ -45,19 +50,54 @@ function PreviewCard({
const { user } = useSession();
const isFocused = useIsFocused();
const [storyIsSaved, setStoryIsSaved] = useState(false);
+ const { channels, initializeChannel, publish } = usePubSub();
+
+ const savedStoryImageComponent = useMemo(() => {
+ return (
+
+ )
+ }, [])
+ const saveStoryImageComponent = useMemo(() => {
+ return (
+
+ )
+ }, [])
useEffect(() => {
- isStoryInReadingList(storyId, user?.id).then(storyInReadingList => setStoryIsSaved(storyInReadingList))
- }, [storyId, isFocused])
+ 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])
+ }
+ }, [channels[storyId]]);
+
+
+ useEffect(() => {
+ isStoryInReadingList(storyId, user?.id).then(storyInReadingList =>
+ setStoryIsSaved(storyInReadingList),
+ );
+ }, [storyId, isFocused]);
- const saveStory = () => {
- if (storyIsSaved) {
- deleteUserStoryToReadingList(user?.id, storyId);
+ const saveStory = async (saved: boolean) => {
+ setStoryIsSaved(saved);
+ publish(storyId, saved);
+ if (saved) {
+ await addUserStoryToReadingList(user?.id, storyId);
} else {
- addUserStoryToReadingList(user?.id, storyId);
+ await deleteUserStoryToReadingList(user?.id, storyId);
}
- setStoryIsSaved(!storyIsSaved);
};
return (
@@ -67,11 +107,10 @@ function PreviewCard({
{title}
- saveStory()}>
-
+ saveStory(!storyIsSaved)}>
+ {storyIsSaved ?
+ savedStoryImageComponent : saveStoryImageComponent
+ }
diff --git a/src/components/PreviewCard/styles.ts b/src/components/PreviewCard/styles.ts
index ed355c7e..d8d3ef1d 100644
--- a/src/components/PreviewCard/styles.ts
+++ b/src/components/PreviewCard/styles.ts
@@ -50,9 +50,11 @@ const styles = StyleSheet.create({
paddingTop: 16,
paddingLeft: 12,
paddingRight: 12,
+ paddingBottom: 8,
borderBottomColor: '#EBEBEB',
borderBottomWidth: StyleSheet.hairlineWidth,
flexDirection: 'row',
+ flexGrow: 1,
justifyContent: 'space-between',
},
tag: {
diff --git a/src/queries/savedStories.tsx b/src/queries/savedStories.tsx
index 8b884607..78c4e53d 100644
--- a/src/queries/savedStories.tsx
+++ b/src/queries/savedStories.tsx
@@ -2,10 +2,9 @@ import supabase from '../utils/supabase';
enum SavedList {
FAVORITES = 'favorites',
- READING_LIST = 'reading list'
+ READING_LIST = 'reading list',
}
-
async function fetchUserStories(
user_id: string | undefined,
name: string | undefined,
@@ -65,13 +64,13 @@ async function addUserStory(
) {
const { error } = await supabase
.from('saved_stories')
- .insert([{ user_id: user_id, story_id: story_id, name: name }])
+ .upsert([{ user_id: user_id, story_id: story_id, name: name }])
.select();
if (error) {
if (process.env.NODE_ENV !== 'production') {
throw new Error(
- `An error occured when trying to set user saved stories: ${error.details}`,
+ `An error occured when trying to set user saved stories: ${JSON.stringify(error)}`,
);
}
}
@@ -105,7 +104,6 @@ export async function deleteUserStoryToReadingList(
deleteUserStory(user_id, story_id, SavedList.READING_LIST);
}
-
export async function deleteUserStory(
user_id: string | undefined,
story_id: number,
@@ -127,16 +125,18 @@ export async function deleteUserStory(
}
}
-export async function isStoryInReadingList(storyId: number, userId: string | undefined): Promise {
- let { data, error } = await supabase
- .rpc('is_story_saved_for_user', {
- list_name: "reading list",
- story_db_id: storyId,
- user_uuid: userId,
- })
+export async function isStoryInReadingList(
+ storyId: number,
+ userId: string | undefined,
+): Promise {
+ let { data, error } = await supabase.rpc('is_story_saved_for_user', {
+ list_name: 'reading list',
+ story_db_id: storyId,
+ user_uuid: userId,
+ });
if (error) {
- console.error(error)
+ console.error(error);
return false;
}
diff --git a/src/utils/AuthContext.tsx b/src/utils/AuthContext.tsx
index 4273a1cb..34d711da 100644
--- a/src/utils/AuthContext.tsx
+++ b/src/utils/AuthContext.tsx
@@ -31,13 +31,13 @@ export interface AuthState {
resendVerification: (email: string) => Promise;
resetPassword: (email: string) => Promise<
| {
- data: object;
- error: null;
- }
+ data: object;
+ error: null;
+ }
| {
- data: null;
- error: AuthError;
- }
+ data: null;
+ error: AuthError;
+ }
>;
updateUser: (attributes: UserAttributes) => Promise;
signOut: () => Promise;
diff --git a/src/utils/PubSubContext.tsx b/src/utils/PubSubContext.tsx
new file mode 100644
index 00000000..5bfe68ae
--- /dev/null
+++ b/src/utils/PubSubContext.tsx
@@ -0,0 +1,60 @@
+import React, {
+ createContext,
+ useContext,
+ useMemo,
+ useState,
+} from 'react';
+
+export interface PubSubState {
+ channels: Record;
+ initializeChannel: (id: number) => void;
+ publish: (id: number, message: boolean) => void;
+}
+
+const BooleanPubSubContext = createContext({} as PubSubState);
+
+export function usePubSub() {
+ const value = useContext(BooleanPubSubContext);
+ if (process.env.NODE_ENV !== 'production') {
+ if (!value) {
+ throw new Error(
+ 'usePubSub must be wrapped in a ',
+ );
+ }
+ }
+
+ return value;
+}
+
+export function BooleanPubSubProvider({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const [channels, setChannels] = useState>({})
+
+ const initializeChannel = (id: number) => {
+ if (!(id in channels)) {
+ setChannels({ ...channels, [id]: undefined })
+ }
+ }
+
+ const publish = (id: number, message: boolean) => {
+ setChannels({ ...channels, [id]: message })
+ }
+
+ const authContextValue = useMemo(
+ () => ({
+ channels,
+ initializeChannel,
+ publish,
+ }),
+ [channels],
+ );
+
+ return (
+
+ {children}
+
+ );
+}