Enable fetching instance search results on improved search interface

This commit is contained in:
Nat 2021-05-22 08:09:30 -03:00
parent f133458df6
commit cf09107cf4
3 changed files with 298 additions and 124 deletions

View File

@ -1,66 +1,179 @@
import React, { useState } from "react"; import React, { useState, useEffect } from "react";
import { View, TextInput, Text, Dimensions, Image } from "react-native"; import {
View,
TextInput,
Text,
Dimensions,
Image,
} from "react-native";
import { TabView, TabBar, SceneMap } from "react-native-tab-view";
import { FontAwesome } from '@expo/vector-icons';
import AsyncStorage from "@react-native-async-storage/async-storage";
import * as requests from "src/requests";
import { StatusBarSpace } from "src/interface/rendering"; import { StatusBarSpace } from "src/interface/rendering";
import { ScreenWithTrayJsx } from "src/components/navigation/navigators"; import { ScreenWithTrayJsx } from "src/components/navigation/navigators";
import { TouchableWithoutFeedback } from "react-native-gesture-handler"; import { TouchableOpacity } from "react-native-gesture-handler";
const TEST_IMAGE = "https://cache.desktopnexus.com/thumbseg/2255/2255124-bigthumbnail.jpg";
const TEST_ACCOUNTS = [
{
id: 1,
avatar: TEST_IMAGE,
username: "njms",
acct: "njms",
display_name: "Nat🔆",
},
{
id: 2,
avatar: TEST_IMAGE,
username: "njms",
acct: "njms",
display_name: "Nat🔆",
}
];
const TEST_HASHTAGS = [
{
id: 1,
name: "hashtag1",
},
{
id: 2,
name: "hashtag2",
},
];
function navCallbackFactory(navigation, route) { function navCallbackFactory(navigation, route) {
return params => { return params => {
console.log("test")
navigation.navigate(route, params); navigation.navigate(route, params);
} }
} }
const SearchJsx = ({navigation}) => { const SearchJsx = ({navigation}) => {
// The number of additional items to fetch each time
const FETCH_LIMIT = 5;
let [state, setState] = useState({ let [state, setState] = useState({
query: "", query: "",
loaded: false,
accountOffset: 0,
hashtagOffset: 0,
}); });
const accountCallback = navCallbackFactory(navigation, "ViewProfile"); useEffect(() => {
const hashtagCallback = navCallbackFactory(navigation, "ViewHashtag"); let instance, accessToken;
AsyncStorage
.multiGet([
"@user_instance",
"@user_token",
])
.then(([instancePair, tokenPair]) => {
instance = instancePair[1];
accessToken = JSON.parse(tokenPair[1]).access_token;
setState({...state,
instance,
accessToken,
loaded: true,
});
});
}, []);
const _handleSearch = async () => {
const results = await requests.fetchSearchResults(
state.instance,
state.accessToken,
{
q: state.query,
limit: FETCH_LIMIT,
}
);
setState({...state,
results,
accountOffset: FETCH_LIMIT,
hashtagOffset: FETCH_LIMIT,
});
};
const _handleShowMoreAccounts = async () => {
const { accounts } = await requests.fetchSearchResults(
state.instance,
state.accessToken,
{
q: state.query,
type: "accounts",
offset: state.accountOffset,
limit: FETCH_LIMIT,
}
);
setState({...state,
results: {...state.results,
accounts: state.results.accounts.concat(accounts),
},
accountOffset: state.accountOffset + FETCH_LIMIT,
});
};
const _handleShowMoreHashtags = async () => {
const { hashtags } = await requests.fetchSearchResults(
state.instance,
state.accessToken,
{
q: state.query,
type: "hashtags",
offset: state.hashtagOffset,
limit: FETCH_LIMIT,
}
);
setState({...state,
results: {...state.results,
hashtags: state.results.hashtags.concat(hashtags),
},
hashtagOffset: state.hashtagOffset + FETCH_LIMIT,
});
};
const [ index, setIndex ] = useState(0);
const [ routes ] = useState([
{
key: "accounts",
icon: "user",
},
{
key: "hashtags",
icon: "hashtag",
},
]);
const AccountRenderer = () => (
<AccountListJsx
callback = { navCallbackFactory(navigation, "ViewProfile") }
onShowMore = { _handleShowMoreAccounts }
data = { state.results.accounts }
offset = { state.accountOffset } />
);
const HashtagRenderer = () => (
<HashtagListJsx
callback = { navCallbackFactory(navigation, "ViewHashtag") }
onShowMore = { _handleShowMoreHashtags }
data = { state.results.hashtags }
offset = { state.hashtagOffset } />
);
const renderScene = SceneMap({
accounts: AccountRenderer,
hashtags: HashtagRenderer,
});
const renderTabBar = (props) => (
<TabBar
{ ...props }
indicatorStyle = { styles.tabBar.indicator }
activeColor = "#000"
inactiveColor = "#888"
renderIcon = { renderIcon }
style = { styles.tabBar.tab } />
);
const renderIcon = ({ route, color }) => (
<FontAwesome
name = { route.icon }
size = { 24 }
color = { color } />
);
return ( return (
<ScreenWithTrayJsx <>
{ state.loaded
? <ScreenWithTrayJsx
active = "Discover" active = "Discover"
statusBarColor = "white"
navigation = { navigation }> navigation = { navigation }>
<StatusBarSpace /> <View style = { styles.form.container }>
<View style = { styles.form }>
<TextInput <TextInput
style = { styles.searchBar } style = { styles.form.input }
placeholder = "Search..." placeholder = "Search..."
autoFocus autoFocus
onChangeText = { q => setState({query: q}) } onChangeText = {
q => setState({ ...state, query: q })
}
onBlur = { onBlur = {
() => { () => {
if (state.query == "") { if (state.query == "") {
@ -69,28 +182,32 @@ const SearchJsx = ({navigation}) => {
} }
} }
value = { state.query } /> value = { state.query } />
<TouchableOpacity
onPress = { _handleSearch }
style = { styles.form.submit }>
<FontAwesome name="search" size={24} color="black" />
</TouchableOpacity>
</View> </View>
{ state.query == "" ? { state.results
<View></View> ? <TabView
: <View> navigationState = { { index, routes } }
<Text style = { styles.label }>Accounts</Text> renderScene = { renderScene }
<AccountsListJsx renderTabBar = { renderTabBar }
data = { TEST_ACCOUNTS } onIndexChange = { setIndex }
callback = { accountCallback } /> initialLayout = { { width: SCREEN_WIDTH } } />
<Text style = { styles.label }>Hashtags</Text> : <></>
<HashtagListJsx
data = { TEST_HASHTAGS }
callback = { hashtagCallback } />
</View>
} }
</ScreenWithTrayJsx> </ScreenWithTrayJsx>
: <></>
}
</>
); );
}; };
const SearchItemJsx = (props) => { const SearchItemJsx = (props) => {
return ( return (
<TouchableWithoutFeedback <TouchableOpacity
onPress = { () => props.callback(props.params) }> onPress = { () => props.callback(props.navParams) }>
<View style = { styles.searchResultContainer }> <View style = { styles.searchResultContainer }>
<Image <Image
style = { styles.thumbnail } style = { styles.thumbnail }
@ -99,13 +216,14 @@ const SearchItemJsx = (props) => {
{ props.children } { props.children }
</View> </View>
</View> </View>
</TouchableWithoutFeedback> </TouchableOpacity>
); );
}; };
const AccountsListJsx = (props) => { const AccountListJsx = (props) => {
return ( return (
<View style = { styles.searchList }> <View style = { styles.searchList }>
<>
{ {
props.data.map(item => { props.data.map(item => {
return ( return (
@ -113,9 +231,9 @@ const AccountsListJsx = (props) => {
key = { item.id } key = { item.id }
thumbnail = { { uri: item.avatar } } thumbnail = { { uri: item.avatar } }
callback = { props.callback } callback = { props.callback }
params = { { acct: item.acct } }> navParams = { { profile: item } }>
<Text style = { styles.username }> <Text style = { styles.username }>
{ item.username } { item.acct }
</Text> </Text>
<Text style = { styles.displayName }> <Text style = { styles.displayName }>
{ item.display_name } { item.display_name }
@ -124,6 +242,20 @@ const AccountsListJsx = (props) => {
); );
}) })
} }
</>
<>
{ props.data.length == props.offset
? <View style = { styles.showMore.container }>
<TouchableOpacity
onPress = { props.onShowMore }>
<View style = { styles.showMore.button }>
<Text>Show more?</Text>
</View>
</TouchableOpacity>
</View>
: <></>
}
</>
</View> </View>
); );
}; };
@ -131,14 +263,15 @@ const AccountsListJsx = (props) => {
const HashtagListJsx = (props) => { const HashtagListJsx = (props) => {
return ( return (
<View style = { styles.searchList }> <View style = { styles.searchList }>
<>
{ {
props.data.map(item => { props.data.map((item, i) => {
return ( return (
<SearchItemJsx <SearchItemJsx
key = { item.id } key = { i }
thumbnail = { require("assets/hashtag.png") } thumbnail = { require("assets/hashtag.png") }
callback = { props.callback } callback = { props.callback }
params = { { name: item.name } }> navParams = { { hashtag: item } }>
<Text style = { styles.username }> <Text style = { styles.username }>
#{ item.name } #{ item.name }
</Text> </Text>
@ -146,22 +279,43 @@ const HashtagListJsx = (props) => {
); );
}) })
} }
</>
<>
{ props.data.length == props.offset
? <View style = { styles.showMore.container }>
<TouchableOpacity
onPress = { props.onShowMore }>
<View style = { styles.showMore.button }>
<Text>Show more?</Text>
</View>
</TouchableOpacity>
</View>
:<></>
}
</>
</View> </View>
); );
} }
const SCREEN_WIDTH = Dimensions.get("window").width;
const styles = { const styles = {
form: { form: {
display: "flex", container: {
flexDirection: "row",
justifyContent: "center", justifyContent: "center",
backgroundColor: "white", backgroundColor: "white",
padding: 20 padding: 20,
}, },
searchBar: { input: {
flexGrow: 1,
padding: 10, padding: 10,
fontSize: 17, fontSize: 17,
color: "#888" color: "#888"
}, },
submit: {
padding: 20,
}
},
label: { label: {
padding: 10, padding: 10,
fontSize: 15, fontSize: 15,
@ -184,7 +338,25 @@ const styles = {
height: 50, height: 50,
borderRadius: 25, borderRadius: 25,
marginRight: 10, marginRight: 10,
} },
showMore: {
container: {
justifyContent: "center",
alignItems: "center"
},
button: {
borderWidth: 1,
borderColor: "#888",
borderRadius: 5,
padding: 10,
margin: 20
},
},
tabBar: {
indicator: { backgroundColor: "black" },
tab: { backgroundColor: "white" },
},
} }
export default SearchJsx; export default SearchJsx;

View File

@ -71,29 +71,22 @@ const ViewProfileJsx = ({navigation}) => {
.multiGet(["@user_profile", "@user_instance", "@user_token"]) .multiGet(["@user_profile", "@user_instance", "@user_token"])
.then(([ ownProfilePair, ownDomainPair, tokenPair ]) => { .then(([ ownProfilePair, ownDomainPair, tokenPair ]) => {
ownProfile = JSON.parse(ownProfilePair[1]); ownProfile = JSON.parse(ownProfilePair[1]);
ownDomain = ownDomainPair[1]; instance = ownDomainPair[1];
accessToken = JSON.parse(tokenPair[1]).access_token; accessToken = JSON.parse(tokenPair[1]).access_token;
const parsedAcct = state.profile.acct.split("@");
domain = parsedAcct.length == 1
? ownDomain // There's no @ in the acct, thus it's a local user
: parsedAcct [1] // The part of profile.acct after the @
return Promise.all([ return Promise.all([
requests.fetchFollowing( requests.fetchFollowing(
ownDomain, instance,
ownProfile.id, ownProfile.id,
accessToken accessToken
), ),
requests.fetchFollowers( requests.fetchFollowers(
domain, instance,
state.profile.id, state.profile.id,
accessToken accessToken
), ),
requests.fetchAccountStatuses( requests.fetchAccountStatuses(
// NOTE: Should be fetched from remote instance if instance,
// necessary Thus, we use domain and not ownDomain
domain,
state.profile.id, state.profile.id,
accessToken accessToken
) )

View File

@ -55,7 +55,7 @@ export async function postForm(url, data, token = false) {
export async function get(url, token = false, data = false) { export async function get(url, token = false, data = false) {
let completeURL; let completeURL;
if (data) { if (data) {
let params = new URLSearchParams(data) let params = new URLSearchParams(data);
completeURL = `${url}?${params.toString()}`; completeURL = `${url}?${params.toString()}`;
} else { } else {
completeURL = url; completeURL = url;
@ -113,3 +113,12 @@ export async function fetchPublicTimeline(domain, token, params = false) {
); );
return resp.json(); return resp.json();
} }
export async function fetchSearchResults(domain, token, params) {
const resp = await get(
`https://${domain}/api/v2/search`,
token,
params
);
return resp.json();
}