diff options
-rw-r--r-- | ios/Podfile.lock | 6 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | src/components/common/index.ts | 1 | ||||
-rw-r--r-- | src/components/search/RecentSearches.tsx | 52 | ||||
-rw-r--r-- | src/components/search/SearchResult.tsx | 53 | ||||
-rw-r--r-- | src/routes/authentication/AuthProvider.tsx | 25 | ||||
-rw-r--r-- | src/screens/search/SearchScreen.tsx | 53 | ||||
-rw-r--r-- | yarn.lock | 19 |
8 files changed, 206 insertions, 4 deletions
diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 87cc0258..54909b3d 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -298,6 +298,8 @@ PODS: - ReactCommon/callinvoker (= 0.62.2) - rn-fetch-blob (0.12.0): - React-Core + - RNCAsyncStorage (1.12.0): + - React - RNCMaskedView (0.1.10): - React - RNGestureHandler (1.7.0): @@ -374,6 +376,7 @@ DEPENDENCIES: - ReactCommon/callinvoker (from `../node_modules/react-native/ReactCommon`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - rn-fetch-blob (from `../node_modules/rn-fetch-blob`) + - "RNCAsyncStorage (from `../node_modules/@react-native-community/async-storage`)" - "RNCMaskedView (from `../node_modules/@react-native-community/masked-view`)" - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - RNImageCropPicker (from `../node_modules/react-native-image-crop-picker`) @@ -454,6 +457,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" rn-fetch-blob: :path: "../node_modules/rn-fetch-blob" + RNCAsyncStorage: + :path: "../node_modules/@react-native-community/async-storage" RNCMaskedView: :path: "../node_modules/@react-native-community/masked-view" RNGestureHandler: @@ -510,6 +515,7 @@ SPEC CHECKSUMS: React-RCTVibration: 4356114dbcba4ce66991096e51a66e61eda51256 ReactCommon: ed4e11d27609d571e7eee8b65548efc191116eb3 rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba + RNCAsyncStorage: 2a692bcb9b69b76a2f1a95f33db057129700af64 RNCMaskedView: 5a8ec07677aa885546a0d98da336457e2bea557f RNGestureHandler: b6b359bb800ae399a9c8b27032bdbf7c18f08a08 RNImageCropPicker: f0557a908758c4a3f83978894ec7227651529b45 diff --git a/package.json b/package.json index 7f47e1af..96c7d401 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix" }, "dependencies": { + "@react-native-community/async-storage": "^1.12.0", "@react-native-community/masked-view": "^0.1.10", "@react-navigation/bottom-tabs": "^5.7.2", "@react-navigation/native": "^5.6.1", diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 63a7b9c2..c9c4f27a 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -6,3 +6,4 @@ export {default as GradientBackground} from './GradientBackground'; export {default as Post} from './post'; export {default as SocialIcon} from './SocialIcon'; export {default as TabsGradient} from './TabsGradient'; +export {default as RecentSearches} from '../search/RecentSearches'; diff --git a/src/components/search/RecentSearches.tsx b/src/components/search/RecentSearches.tsx new file mode 100644 index 00000000..a5c08984 --- /dev/null +++ b/src/components/search/RecentSearches.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + TouchableOpacityProps, +} from 'react-native'; +import {ProfilePreviewType} from 'src/types'; +import SearchResults from './SearchResults'; + +interface RecentSearchesProps extends TouchableOpacityProps { + sectionTitle: string; + sectionButtonTitle: string; + recents: Array<ProfilePreviewType>; +} +/** + * An image component that returns the <Image> of the icon for a specific social media platform. + */ +const RecentSearches: React.FC<RecentSearchesProps> = (props) => { + return ( + <> + <View style={styles.container}> + <Text style={styles.title}>{props.sectionTitle}</Text> + {props.sectionButtonTitle && ( + <TouchableOpacity {...props}> + <Text style={styles.clear}>Clear all</Text> + </TouchableOpacity> + )} + </View> + <SearchResults results={props.recents} /> + </> + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + }, + title: { + fontSize: 17, + fontWeight: 'bold', + flexGrow: 1, + }, + clear: { + fontSize: 17, + fontWeight: 'bold', + color: '#698DD3', + }, +}); + +export default RecentSearches; diff --git a/src/components/search/SearchResult.tsx b/src/components/search/SearchResult.tsx index 60c22d41..e65be1f4 100644 --- a/src/components/search/SearchResult.tsx +++ b/src/components/search/SearchResult.tsx @@ -9,6 +9,7 @@ import { TouchableOpacity, } from 'react-native'; import RNFetchBlob from 'rn-fetch-blob'; +import AsyncStorage from '@react-native-community/async-storage'; import {AVATAR_PHOTO_ENDPOINT} from '../../constants'; interface SearchResultProps extends ViewProps { @@ -48,8 +49,58 @@ const SearchResult: React.FC<SearchResultProps> = ({ }; }, [id]); + /** + * Adds a searched user to the recently searched cache if they're tapped on. + * Cache maintains 10 recently searched users, popping off the oldest one if + * needed to make space. + */ + const addToRecentlyStored = async () => { + let user: ProfilePreviewType = { + id, + username, + first_name, + last_name, + }; + try { + const jsonValue = await AsyncStorage.getItem('@recently_searched_users'); + let recentlySearchedList = + jsonValue != null ? JSON.parse(jsonValue) : null; + if (recentlySearchedList) { + if (recentlySearchedList.length > 0) { + if ( + recentlySearchedList.some( + (saved_user: ProfilePreviewType) => saved_user.id === id, + ) + ) { + console.log('User already in recently searched.'); + } else { + if (recentlySearchedList.length >= 10) { + recentlySearchedList.pop(); + } + recentlySearchedList.unshift(user); + } + } + } else { + recentlySearchedList = [user]; + } + try { + let recentlySearchedListString = JSON.stringify(recentlySearchedList); + await AsyncStorage.setItem( + '@recently_searched_users', + recentlySearchedListString, + ); + } catch (e) { + console.log(e); + } + } catch (e) { + console.log(e); + } + }; + return ( - <TouchableOpacity style={[styles.container, style]}> + <TouchableOpacity + onPress={addToRecentlyStored} + style={[styles.container, style]}> <Image style={styles.avatar} source={ diff --git a/src/routes/authentication/AuthProvider.tsx b/src/routes/authentication/AuthProvider.tsx index e52d56bc..589cb051 100644 --- a/src/routes/authentication/AuthProvider.tsx +++ b/src/routes/authentication/AuthProvider.tsx @@ -1,7 +1,13 @@ import React, {useEffect} from 'react'; import {createContext, useState} from 'react'; import RNFetchBlob from 'rn-fetch-blob'; -import {UserType, ProfileType, InstagramPostType} from '../../types'; +import AsyncStorage from '@react-native-community/async-storage'; +import { + UserType, + ProfileType, + InstagramPostType, + ProfilePreviewType, +} from '../../types'; import { PROFILE_INFO_ENDPOINT, AVATAR_PHOTO_ENDPOINT, @@ -17,6 +23,7 @@ interface AuthContextProps { avatar: string | null; cover: string | null; instaPosts: Array<InstagramPostType>; + recentSearches: Array<ProfilePreviewType>; } const NO_USER: UserType = { userId: '', @@ -35,6 +42,7 @@ export const AuthContext = createContext<AuthContextProps>({ avatar: null, cover: null, instaPosts: [], + recentSearches: [], }); /** @@ -46,6 +54,9 @@ const AuthProvider: React.FC = ({children}) => { const [avatar, setAvatar] = useState<string | null>(null); const [cover, setCover] = useState<string | null>(null); const [instaPosts, setInstaPosts] = useState<Array<InstagramPostType>>([]); + const [recentSearches, setRecentSearches] = useState< + Array<ProfilePreviewType> + >([]); const {userId} = user; useEffect(() => { @@ -115,10 +126,21 @@ const AuthProvider: React.FC = ({children}) => { console.log(error); } }; + const loadRecentlySearchedUsers = async () => { + try { + const asyncCache = await AsyncStorage.getItem( + '@recently_searched_users', + ); + asyncCache != null ? setRecentSearches(JSON.parse(asyncCache)) : null; + } catch (e) { + console.log(e); + } + }; loadProfileInfo(); loadAvatar(); loadCover(); loadInstaPosts(); + loadRecentlySearchedUsers(); }, [userId]); return ( @@ -135,6 +157,7 @@ const AuthProvider: React.FC = ({children}) => { logout: () => { setUser(NO_USER); }, + recentSearches, }}> {children} </AuthContext.Provider> diff --git a/src/screens/search/SearchScreen.tsx b/src/screens/search/SearchScreen.tsx index 94b9ab41..d85c0a90 100644 --- a/src/screens/search/SearchScreen.tsx +++ b/src/screens/search/SearchScreen.tsx @@ -8,11 +8,14 @@ import { SearchResultsBackground, SearchResults, TabsGradient, + RecentSearches, } from '../../components'; import {SCREEN_HEIGHT, StatusBarHeight} from '../../utils'; import Animated, {Easing, timing} from 'react-native-reanimated'; +import AsyncStorage from '@react-native-community/async-storage'; import {ProfilePreviewType} from '../../types'; import {SEARCH_ENDPOINT} from '../../constants'; +import {AuthContext} from '../../routes/authentication'; const {Value} = Animated; /** @@ -22,8 +25,12 @@ const {Value} = Animated; const top: Animated.Value<number> = new Value(-SCREEN_HEIGHT); const SearchScreen: React.FC = () => { + const {recentSearches} = React.useContext(AuthContext); const [query, setQuery] = useState<string>(''); const [results, setResults] = useState<Array<ProfilePreviewType>>([]); + const [recents, setRecents] = useState<Array<ProfilePreviewType>>( + recentSearches, + ); useEffect(() => { if (query.length < 3) { setResults([]); @@ -66,6 +73,26 @@ const SearchScreen: React.FC = () => { }; timing(top, topOutConfig).start(); }; + const loadRecentlySearchedUsers = async () => { + try { + const asyncCache = await AsyncStorage.getItem('@recently_searched_users'); + asyncCache != null ? setRecents(JSON.parse(asyncCache)) : setRecents([]); + } catch (e) { + console.log(e); + } + }; + const clearRecentlySearched = async () => { + try { + await AsyncStorage.removeItem('@recently_searched_users'); + loadRecentlySearchedUsers(); + } catch (e) { + console.log(e); + } + }; + const handleUpdate = async (val: string) => { + setQuery(val); + loadRecentlySearchedUsers(); + }; return ( <SearchBackground> @@ -79,7 +106,7 @@ const SearchScreen: React.FC = () => { <SearchBar style={styles.searchBar} onCancel={handleBlur} - onChangeText={setQuery} + onChangeText={handleUpdate} onBlur={Keyboard.dismiss} onFocus={handleFocus} value={query} @@ -87,7 +114,16 @@ const SearchScreen: React.FC = () => { /> <Explore /> <SearchResultsBackground {...{top}}> - <SearchResults {...{results}} /> + {results.length === 0 && recents.length !== 0 ? ( + <RecentSearches + sectionTitle="Recent" + sectionButtonTitle="Clear all" + onPress={clearRecentlySearched} + recents={recents} + /> + ) : ( + <SearchResults {...{results}} /> + )} </SearchResultsBackground> </ScrollView> <TabsGradient /> @@ -108,5 +144,18 @@ const styles = StyleSheet.create({ marginVertical: 20, zIndex: 1, }, + recentsHeaderContainer: { + flexDirection: 'row', + }, + recentsHeader: { + fontSize: 17, + fontWeight: 'bold', + flexGrow: 1, + }, + clear: { + fontSize: 17, + fontWeight: 'bold', + color: '#698DD3', + }, }); export default SearchScreen; @@ -877,6 +877,13 @@ "@types/yargs" "^15.0.0" chalk "^3.0.0" +"@react-native-community/async-storage@^1.12.0": + version "1.12.0" + resolved "https://registry.yarnpkg.com/@react-native-community/async-storage/-/async-storage-1.12.0.tgz#d2fc65bc08aa1c3e9514bbe9fe7095eab8e8aca3" + integrity sha512-y3zVxuVyiOxI8TXrvajmYfDbIt2vFNxzV5MiA28v15DQTxDk6uJH3rpc9my+la7u2Tiwt3PpdU2+59ZgZ4h7wA== + dependencies: + deep-assign "^3.0.0" + "@react-native-community/cli-debugger-ui@^4.9.0": version "4.9.0" resolved "https://registry.yarnpkg.com/@react-native-community/cli-debugger-ui/-/cli-debugger-ui-4.9.0.tgz#4177764ba69243c97aa26829d59d9501acb2bd71" @@ -2225,6 +2232,13 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= +deep-assign@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/deep-assign/-/deep-assign-3.0.0.tgz#c8e4c4d401cba25550a2f0f486a2e75bc5f219a2" + integrity sha512-YX2i9XjJ7h5q/aQ/IM9PEwEnDqETAIYbggmdDB3HLTlSgo1CxPsj6pvhPG68rq6SVE0+p+6Ywsm5fTYNrYtBWw== + dependencies: + is-obj "^1.0.0" + deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" @@ -3507,6 +3521,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" |