Enable fetching instance search results on improved search interface
This commit is contained in:
parent
f133458df6
commit
cf09107cf4
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue